chore: import upstream snapshot with attribution
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
// Package broker is an interface used for asynchronous messaging
|
||||
package broker
|
||||
|
||||
// Broker is an interface used for asynchronous messaging.
|
||||
type Broker interface {
|
||||
Init(...Option) error
|
||||
Options() Options
|
||||
Address() string
|
||||
Connect() error
|
||||
Disconnect() error
|
||||
Publish(topic string, m *Message, opts ...PublishOption) error
|
||||
Subscribe(topic string, h Handler, opts ...SubscribeOption) (Subscriber, error)
|
||||
String() string
|
||||
}
|
||||
|
||||
// Handler is used to process messages via a subscription of a topic.
|
||||
// The handler is passed a publication interface which contains the
|
||||
// message and optional Ack method to acknowledge receipt of the message.
|
||||
type Handler func(Event) error
|
||||
|
||||
// Message is a message send/received from the broker.
|
||||
type Message struct {
|
||||
Header map[string]string
|
||||
Body []byte
|
||||
}
|
||||
|
||||
// Event is given to a subscription handler for processing.
|
||||
type Event interface {
|
||||
Topic() string
|
||||
Message() *Message
|
||||
Ack() error
|
||||
Error() error
|
||||
}
|
||||
|
||||
// Subscriber is a convenience return type for the Subscribe method.
|
||||
type Subscriber interface {
|
||||
Options() SubscribeOptions
|
||||
Topic() string
|
||||
Unsubscribe() error
|
||||
}
|
||||
|
||||
var (
|
||||
// DefaultBroker is the default Broker.
|
||||
DefaultBroker = NewHttpBroker()
|
||||
)
|
||||
|
||||
func Init(opts ...Option) error {
|
||||
return DefaultBroker.Init(opts...)
|
||||
}
|
||||
|
||||
func Connect() error {
|
||||
return DefaultBroker.Connect()
|
||||
}
|
||||
|
||||
func Disconnect() error {
|
||||
return DefaultBroker.Disconnect()
|
||||
}
|
||||
|
||||
func Publish(topic string, msg *Message, opts ...PublishOption) error {
|
||||
return DefaultBroker.Publish(topic, msg, opts...)
|
||||
}
|
||||
|
||||
func Subscribe(topic string, handler Handler, opts ...SubscribeOption) (Subscriber, error) {
|
||||
return DefaultBroker.Subscribe(topic, handler, opts...)
|
||||
}
|
||||
|
||||
// String returns the name of the Broker.
|
||||
func String() string {
|
||||
return DefaultBroker.String()
|
||||
}
|
||||
+708
@@ -0,0 +1,708 @@
|
||||
package broker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go-micro.dev/v6/codec/json"
|
||||
merr "go-micro.dev/v6/errors"
|
||||
maddr "go-micro.dev/v6/internal/util/addr"
|
||||
mnet "go-micro.dev/v6/internal/util/net"
|
||||
mls "go-micro.dev/v6/internal/util/tls"
|
||||
"go-micro.dev/v6/registry"
|
||||
"go-micro.dev/v6/registry/cache"
|
||||
"go-micro.dev/v6/transport/headers"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
// HTTP Broker is a point to point async broker.
|
||||
type httpBroker struct {
|
||||
opts Options
|
||||
|
||||
r registry.Registry
|
||||
|
||||
mux *http.ServeMux
|
||||
|
||||
c *http.Client
|
||||
subscribers map[string][]*httpSubscriber
|
||||
exit chan chan error
|
||||
|
||||
inbox map[string][][]byte
|
||||
id string
|
||||
address string
|
||||
|
||||
sync.RWMutex
|
||||
|
||||
// offline message inbox
|
||||
mtx sync.RWMutex
|
||||
running bool
|
||||
}
|
||||
|
||||
type httpSubscriber struct {
|
||||
opts SubscribeOptions
|
||||
fn Handler
|
||||
svc *registry.Service
|
||||
hb *httpBroker
|
||||
id string
|
||||
topic string
|
||||
}
|
||||
|
||||
type httpEvent struct {
|
||||
err error
|
||||
m *Message
|
||||
t string
|
||||
}
|
||||
|
||||
var (
|
||||
DefaultPath = "/"
|
||||
DefaultAddress = "127.0.0.1:0"
|
||||
serviceName = "micro.http.broker"
|
||||
broadcastVersion = "ff.http.broadcast"
|
||||
registerTTL = time.Minute
|
||||
registerInterval = time.Second * 30
|
||||
)
|
||||
|
||||
func init() {
|
||||
}
|
||||
|
||||
func newTransport(config *tls.Config) *http.Transport {
|
||||
if config == nil {
|
||||
// Use environment-based config - secure by default
|
||||
config = mls.Config()
|
||||
}
|
||||
|
||||
dialTLS := func(network string, addr string) (net.Conn, error) {
|
||||
return tls.Dial(network, addr, config)
|
||||
}
|
||||
|
||||
t := &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
Dial: (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}).Dial,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
DialTLS: dialTLS,
|
||||
}
|
||||
runtime.SetFinalizer(&t, func(tr **http.Transport) {
|
||||
(*tr).CloseIdleConnections()
|
||||
})
|
||||
|
||||
// setup http2
|
||||
_ = http2.ConfigureTransport(t)
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
func newHttpBroker(opts ...Option) Broker {
|
||||
options := *NewOptions(opts...)
|
||||
|
||||
options.Registry = registry.DefaultRegistry
|
||||
options.Codec = json.Marshaler{}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
// set address
|
||||
addr := DefaultAddress
|
||||
|
||||
if len(options.Addrs) > 0 && len(options.Addrs[0]) > 0 {
|
||||
addr = options.Addrs[0]
|
||||
}
|
||||
|
||||
h := &httpBroker{
|
||||
id: uuid.New().String(),
|
||||
address: addr,
|
||||
opts: options,
|
||||
r: options.Registry,
|
||||
c: &http.Client{Transport: newTransport(options.TLSConfig)},
|
||||
subscribers: make(map[string][]*httpSubscriber),
|
||||
exit: make(chan chan error),
|
||||
mux: http.NewServeMux(),
|
||||
inbox: make(map[string][][]byte),
|
||||
}
|
||||
|
||||
// specify the message handler
|
||||
h.mux.Handle(DefaultPath, h)
|
||||
|
||||
// get optional handlers
|
||||
if h.opts.Context != nil {
|
||||
handlers, ok := h.opts.Context.Value("http_handlers").(map[string]http.Handler)
|
||||
if ok {
|
||||
for pattern, handler := range handlers {
|
||||
h.mux.Handle(pattern, handler)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *httpEvent) Ack() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *httpEvent) Error() error {
|
||||
return h.err
|
||||
}
|
||||
|
||||
func (h *httpEvent) Message() *Message {
|
||||
return h.m
|
||||
}
|
||||
|
||||
func (h *httpEvent) Topic() string {
|
||||
return h.t
|
||||
}
|
||||
|
||||
func (h *httpSubscriber) Options() SubscribeOptions {
|
||||
return h.opts
|
||||
}
|
||||
|
||||
func (h *httpSubscriber) Topic() string {
|
||||
return h.topic
|
||||
}
|
||||
|
||||
func (h *httpSubscriber) Unsubscribe() error {
|
||||
return h.hb.unsubscribe(h)
|
||||
}
|
||||
|
||||
func (h *httpBroker) saveMessage(topic string, msg []byte) {
|
||||
h.mtx.Lock()
|
||||
defer h.mtx.Unlock()
|
||||
|
||||
// get messages
|
||||
c := h.inbox[topic]
|
||||
|
||||
// save message
|
||||
c = append(c, msg)
|
||||
|
||||
// max length 64
|
||||
if len(c) > 64 {
|
||||
c = c[:64]
|
||||
}
|
||||
|
||||
// save inbox
|
||||
h.inbox[topic] = c
|
||||
}
|
||||
|
||||
func (h *httpBroker) getMessage(topic string, num int) [][]byte {
|
||||
h.mtx.Lock()
|
||||
defer h.mtx.Unlock()
|
||||
|
||||
// get messages
|
||||
c, ok := h.inbox[topic]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
// more message than requests
|
||||
if len(c) >= num {
|
||||
msg := c[:num]
|
||||
h.inbox[topic] = c[num:]
|
||||
return msg
|
||||
}
|
||||
|
||||
// reset inbox
|
||||
h.inbox[topic] = nil
|
||||
|
||||
// return all messages
|
||||
return c
|
||||
}
|
||||
|
||||
func (h *httpBroker) subscribe(s *httpSubscriber) error {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
if err := h.r.Register(s.svc, registry.RegisterTTL(registerTTL)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.subscribers[s.topic] = append(h.subscribers[s.topic], s)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *httpBroker) unsubscribe(s *httpSubscriber) error {
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
//nolint:prealloc
|
||||
var subscribers []*httpSubscriber
|
||||
|
||||
// look for subscriber
|
||||
for _, sub := range h.subscribers[s.topic] {
|
||||
// deregister and skip forward
|
||||
if sub == s {
|
||||
_ = h.r.Deregister(sub.svc)
|
||||
continue
|
||||
}
|
||||
// keep subscriber
|
||||
subscribers = append(subscribers, sub)
|
||||
}
|
||||
|
||||
// set subscribers
|
||||
h.subscribers[s.topic] = subscribers
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *httpBroker) run(l net.Listener) {
|
||||
t := time.NewTicker(registerInterval)
|
||||
defer t.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
// heartbeat for each subscriber
|
||||
case <-t.C:
|
||||
h.RLock()
|
||||
for _, subs := range h.subscribers {
|
||||
for _, sub := range subs {
|
||||
_ = h.r.Register(sub.svc, registry.RegisterTTL(registerTTL))
|
||||
}
|
||||
}
|
||||
h.RUnlock()
|
||||
// received exit signal
|
||||
case ch := <-h.exit:
|
||||
ch <- l.Close()
|
||||
h.RLock()
|
||||
for _, subs := range h.subscribers {
|
||||
for _, sub := range subs {
|
||||
_ = h.r.Deregister(sub.svc)
|
||||
}
|
||||
}
|
||||
h.RUnlock()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *httpBroker) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method != http.MethodPost {
|
||||
err := merr.BadRequest("go.micro.broker", "Method not allowed")
|
||||
http.Error(w, err.Error(), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
defer req.Body.Close()
|
||||
|
||||
_ = req.ParseForm()
|
||||
|
||||
b, err := io.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
errr := merr.InternalServerError("go.micro.broker", "Error reading request body: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(errr.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
var m *Message
|
||||
if err = h.opts.Codec.Unmarshal(b, &m); err != nil {
|
||||
errr := merr.InternalServerError("go.micro.broker", "Error parsing request body: %v", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(errr.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
topic := m.Header[headers.Message]
|
||||
// delete(m.Header, ":topic")
|
||||
|
||||
if len(topic) == 0 {
|
||||
errr := merr.InternalServerError("go.micro.broker", "Topic not found")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(errr.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
p := &httpEvent{m: m, t: topic}
|
||||
id := req.Form.Get("id")
|
||||
|
||||
//nolint:prealloc
|
||||
var subs []Handler
|
||||
|
||||
h.RLock()
|
||||
for _, subscriber := range h.subscribers[topic] {
|
||||
if id != subscriber.id {
|
||||
continue
|
||||
}
|
||||
subs = append(subs, subscriber.fn)
|
||||
}
|
||||
h.RUnlock()
|
||||
|
||||
// execute the handler
|
||||
for _, fn := range subs {
|
||||
p.err = fn(p)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *httpBroker) Address() string {
|
||||
h.RLock()
|
||||
defer h.RUnlock()
|
||||
return h.address
|
||||
}
|
||||
|
||||
func (h *httpBroker) Connect() error {
|
||||
h.RLock()
|
||||
if h.running {
|
||||
h.RUnlock()
|
||||
return nil
|
||||
}
|
||||
h.RUnlock()
|
||||
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
var l net.Listener
|
||||
var err error
|
||||
|
||||
if h.opts.Secure || h.opts.TLSConfig != nil {
|
||||
config := h.opts.TLSConfig
|
||||
|
||||
fn := func(addr string) (net.Listener, error) {
|
||||
if config == nil {
|
||||
hosts := []string{addr}
|
||||
|
||||
// check if its a valid host:port
|
||||
if host, _, err := net.SplitHostPort(addr); err == nil {
|
||||
if len(host) == 0 {
|
||||
hosts = maddr.IPs()
|
||||
} else {
|
||||
hosts = []string{host}
|
||||
}
|
||||
}
|
||||
|
||||
// generate a certificate
|
||||
cert, err := mls.Certificate(hosts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config = &tls.Config{Certificates: []tls.Certificate{cert}}
|
||||
}
|
||||
return tls.Listen("tcp", addr, config)
|
||||
}
|
||||
|
||||
l, err = mnet.Listen(h.address, fn)
|
||||
} else {
|
||||
fn := func(addr string) (net.Listener, error) {
|
||||
return net.Listen("tcp", addr)
|
||||
}
|
||||
|
||||
l, err = mnet.Listen(h.address, fn)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addr := h.address
|
||||
h.address = l.Addr().String()
|
||||
|
||||
go func() { _ = http.Serve(l, h.mux) }()
|
||||
go func() {
|
||||
h.run(l)
|
||||
h.Lock()
|
||||
h.opts.Addrs = []string{addr}
|
||||
h.address = addr
|
||||
h.Unlock()
|
||||
}()
|
||||
|
||||
// get registry
|
||||
reg := h.opts.Registry
|
||||
if reg == nil {
|
||||
reg = registry.DefaultRegistry
|
||||
}
|
||||
// set cache
|
||||
h.r = cache.New(reg)
|
||||
|
||||
// set running
|
||||
h.running = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *httpBroker) Disconnect() error {
|
||||
h.RLock()
|
||||
if !h.running {
|
||||
h.RUnlock()
|
||||
return nil
|
||||
}
|
||||
h.RUnlock()
|
||||
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
// stop cache
|
||||
rc, ok := h.r.(cache.Cache)
|
||||
if ok {
|
||||
rc.Stop()
|
||||
}
|
||||
|
||||
// exit and return err
|
||||
ch := make(chan error)
|
||||
h.exit <- ch
|
||||
err := <-ch
|
||||
|
||||
// set not running
|
||||
h.running = false
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *httpBroker) Init(opts ...Option) error {
|
||||
h.RLock()
|
||||
if h.running {
|
||||
h.RUnlock()
|
||||
return errors.New("cannot init while connected")
|
||||
}
|
||||
h.RUnlock()
|
||||
|
||||
h.Lock()
|
||||
defer h.Unlock()
|
||||
|
||||
for _, o := range opts {
|
||||
o(&h.opts)
|
||||
}
|
||||
|
||||
if len(h.opts.Addrs) > 0 && len(h.opts.Addrs[0]) > 0 {
|
||||
h.address = h.opts.Addrs[0]
|
||||
}
|
||||
|
||||
if len(h.id) == 0 {
|
||||
h.id = "go.micro.http.broker-" + uuid.New().String()
|
||||
}
|
||||
|
||||
// get registry
|
||||
reg := h.opts.Registry
|
||||
if reg == nil {
|
||||
reg = registry.DefaultRegistry
|
||||
}
|
||||
|
||||
// get cache
|
||||
if rc, ok := h.r.(cache.Cache); ok {
|
||||
rc.Stop()
|
||||
}
|
||||
|
||||
// set registry
|
||||
h.r = cache.New(reg)
|
||||
|
||||
// reconfigure tls config
|
||||
if c := h.opts.TLSConfig; c != nil {
|
||||
h.c = &http.Client{
|
||||
Transport: newTransport(c),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *httpBroker) Options() Options {
|
||||
return h.opts
|
||||
}
|
||||
|
||||
func (h *httpBroker) Publish(topic string, msg *Message, opts ...PublishOption) error {
|
||||
// create the message first
|
||||
m := &Message{
|
||||
Header: make(map[string]string),
|
||||
Body: msg.Body,
|
||||
}
|
||||
|
||||
for k, v := range msg.Header {
|
||||
m.Header[k] = v
|
||||
}
|
||||
|
||||
m.Header[headers.Message] = topic
|
||||
|
||||
// encode the message
|
||||
b, err := h.opts.Codec.Marshal(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// save the message
|
||||
h.saveMessage(topic, b)
|
||||
|
||||
// now attempt to get the service
|
||||
h.RLock()
|
||||
s, err := h.r.GetService(serviceName)
|
||||
if err != nil {
|
||||
h.RUnlock()
|
||||
return err
|
||||
}
|
||||
h.RUnlock()
|
||||
|
||||
pub := func(node *registry.Node, t string, b []byte) error {
|
||||
scheme := "http"
|
||||
|
||||
// check if secure is added in metadata
|
||||
if node.Metadata["secure"] == "true" {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
vals := url.Values{}
|
||||
vals.Add("id", node.Id)
|
||||
|
||||
uri := fmt.Sprintf("%s://%s%s?%s", scheme, node.Address, DefaultPath, vals.Encode())
|
||||
r, err := h.c.Post(uri, "application/json", bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// discard response body
|
||||
_, _ = io.Copy(io.Discard, r.Body)
|
||||
r.Body.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
srv := func(s []*registry.Service, b []byte) {
|
||||
for _, service := range s {
|
||||
var nodes []*registry.Node
|
||||
|
||||
for _, node := range service.Nodes {
|
||||
// only use nodes tagged with broker http
|
||||
if node.Metadata["broker"] != "http" {
|
||||
continue
|
||||
}
|
||||
|
||||
// look for nodes for the topic
|
||||
if node.Metadata["topic"] != topic {
|
||||
continue
|
||||
}
|
||||
|
||||
nodes = append(nodes, node)
|
||||
}
|
||||
|
||||
// only process if we have nodes
|
||||
if len(nodes) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch service.Version {
|
||||
// broadcast version means broadcast to all nodes
|
||||
case broadcastVersion:
|
||||
var success bool
|
||||
|
||||
// publish to all nodes
|
||||
for _, node := range nodes {
|
||||
// publish async
|
||||
if err := pub(node, topic, b); err == nil {
|
||||
success = true
|
||||
}
|
||||
}
|
||||
|
||||
// save if it failed to publish at least once
|
||||
if !success {
|
||||
h.saveMessage(topic, b)
|
||||
}
|
||||
default:
|
||||
// select node to publish to
|
||||
node := nodes[rand.Int()%len(nodes)]
|
||||
|
||||
// publish async to one node
|
||||
if err := pub(node, topic, b); err != nil {
|
||||
// if failed save it
|
||||
h.saveMessage(topic, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// do the rest async
|
||||
go func() {
|
||||
// get a third of the backlog
|
||||
messages := h.getMessage(topic, 8)
|
||||
delay := (len(messages) > 1)
|
||||
|
||||
// publish all the messages
|
||||
for _, msg := range messages {
|
||||
// serialize here
|
||||
srv(s, msg)
|
||||
|
||||
// sending a backlog of messages
|
||||
if delay {
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *httpBroker) Subscribe(topic string, handler Handler, opts ...SubscribeOption) (Subscriber, error) {
|
||||
var err error
|
||||
var host, port string
|
||||
options := NewSubscribeOptions(opts...)
|
||||
|
||||
// parse address for host, port
|
||||
host, port, err = net.SplitHostPort(h.Address())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addr, err := maddr.Extract(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var secure bool
|
||||
|
||||
if h.opts.Secure || h.opts.TLSConfig != nil {
|
||||
secure = true
|
||||
}
|
||||
|
||||
// register service
|
||||
node := ®istry.Node{
|
||||
Id: topic + "-" + h.id,
|
||||
Address: mnet.HostPort(addr, port),
|
||||
Metadata: map[string]string{
|
||||
"secure": fmt.Sprintf("%t", secure),
|
||||
"broker": "http",
|
||||
"topic": topic,
|
||||
},
|
||||
}
|
||||
|
||||
// check for queue group or broadcast queue
|
||||
version := options.Queue
|
||||
if len(version) == 0 {
|
||||
version = broadcastVersion
|
||||
}
|
||||
|
||||
service := ®istry.Service{
|
||||
Name: serviceName,
|
||||
Version: version,
|
||||
Nodes: []*registry.Node{node},
|
||||
}
|
||||
|
||||
// generate subscriber
|
||||
subscriber := &httpSubscriber{
|
||||
opts: options,
|
||||
hb: h,
|
||||
id: node.Id,
|
||||
topic: topic,
|
||||
fn: handler,
|
||||
svc: service,
|
||||
}
|
||||
|
||||
// subscribe now
|
||||
if err := h.subscribe(subscriber); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// return the subscriber
|
||||
return subscriber, nil
|
||||
}
|
||||
|
||||
func (h *httpBroker) String() string {
|
||||
return "http"
|
||||
}
|
||||
|
||||
// NewHttpBroker returns a new http broker.
|
||||
func NewHttpBroker(opts ...Option) Broker {
|
||||
return newHttpBroker(opts...)
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
package broker_test
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"go-micro.dev/v6/broker"
|
||||
"go-micro.dev/v6/registry"
|
||||
)
|
||||
|
||||
var (
|
||||
// mock data.
|
||||
testData = map[string][]*registry.Service{
|
||||
"foo": {
|
||||
{
|
||||
Name: "foo",
|
||||
Version: "1.0.0",
|
||||
Nodes: []*registry.Node{
|
||||
{
|
||||
Id: "foo-1.0.0-123",
|
||||
Address: "localhost:9999",
|
||||
},
|
||||
{
|
||||
Id: "foo-1.0.0-321",
|
||||
Address: "localhost:9999",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "foo",
|
||||
Version: "1.0.1",
|
||||
Nodes: []*registry.Node{
|
||||
{
|
||||
Id: "foo-1.0.1-321",
|
||||
Address: "localhost:6666",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "foo",
|
||||
Version: "1.0.3",
|
||||
Nodes: []*registry.Node{
|
||||
{
|
||||
Id: "foo-1.0.3-345",
|
||||
Address: "localhost:8888",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func newTestRegistry() registry.Registry {
|
||||
return registry.NewMemoryRegistry(registry.Services(testData))
|
||||
}
|
||||
|
||||
func sub(b *testing.B, c int) {
|
||||
b.StopTimer()
|
||||
m := newTestRegistry()
|
||||
|
||||
brker := broker.NewHttpBroker(broker.Registry(m))
|
||||
topic := uuid.New().String()
|
||||
|
||||
if err := brker.Init(); err != nil {
|
||||
b.Fatalf("Unexpected init error: %v", err)
|
||||
}
|
||||
|
||||
if err := brker.Connect(); err != nil {
|
||||
b.Fatalf("Unexpected connect error: %v", err)
|
||||
}
|
||||
|
||||
msg := &broker.Message{
|
||||
Header: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
Body: []byte(`{"message": "Hello World"}`),
|
||||
}
|
||||
|
||||
var subs []broker.Subscriber
|
||||
done := make(chan bool, c)
|
||||
|
||||
for i := 0; i < c; i++ {
|
||||
sub, err := brker.Subscribe(topic, func(p broker.Event) error {
|
||||
done <- true
|
||||
m := p.Message()
|
||||
|
||||
if string(m.Body) != string(msg.Body) {
|
||||
b.Fatalf("Unexpected msg %s, expected %s", string(m.Body), string(msg.Body))
|
||||
}
|
||||
|
||||
return nil
|
||||
}, broker.Queue("shared"))
|
||||
if err != nil {
|
||||
b.Fatalf("Unexpected subscribe error: %v", err)
|
||||
}
|
||||
subs = append(subs, sub)
|
||||
}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
b.StartTimer()
|
||||
if err := brker.Publish(topic, msg); err != nil {
|
||||
b.Fatalf("Unexpected publish error: %v", err)
|
||||
}
|
||||
<-done
|
||||
b.StopTimer()
|
||||
}
|
||||
|
||||
for _, sub := range subs {
|
||||
if err := sub.Unsubscribe(); err != nil {
|
||||
b.Fatalf("Unexpected unsubscribe error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := brker.Disconnect(); err != nil {
|
||||
b.Fatalf("Unexpected disconnect error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func pub(b *testing.B, c int) {
|
||||
b.StopTimer()
|
||||
m := newTestRegistry()
|
||||
brk := broker.NewHttpBroker(broker.Registry(m))
|
||||
topic := uuid.New().String()
|
||||
|
||||
if err := brk.Init(); err != nil {
|
||||
b.Fatalf("Unexpected init error: %v", err)
|
||||
}
|
||||
|
||||
if err := brk.Connect(); err != nil {
|
||||
b.Fatalf("Unexpected connect error: %v", err)
|
||||
}
|
||||
|
||||
msg := &broker.Message{
|
||||
Header: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
Body: []byte(`{"message": "Hello World"}`),
|
||||
}
|
||||
|
||||
done := make(chan bool, c*4)
|
||||
|
||||
sub, err := brk.Subscribe(topic, func(p broker.Event) error {
|
||||
done <- true
|
||||
m := p.Message()
|
||||
if string(m.Body) != string(msg.Body) {
|
||||
b.Fatalf("Unexpected msg %s, expected %s", string(m.Body), string(msg.Body))
|
||||
}
|
||||
return nil
|
||||
}, broker.Queue("shared"))
|
||||
if err != nil {
|
||||
b.Fatalf("Unexpected subscribe error: %v", err)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
ch := make(chan int, c*4)
|
||||
b.StartTimer()
|
||||
|
||||
for i := 0; i < c; i++ {
|
||||
go func() {
|
||||
for range ch {
|
||||
if err := brk.Publish(topic, msg); err != nil {
|
||||
b.Errorf("Unexpected publish error: %v", err)
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
wg.Done()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
wg.Add(1)
|
||||
ch <- i
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
b.StopTimer()
|
||||
sub.Unsubscribe()
|
||||
close(ch)
|
||||
close(done)
|
||||
|
||||
if err := brk.Disconnect(); err != nil {
|
||||
b.Fatalf("Unexpected disconnect error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBroker(t *testing.T) {
|
||||
m := newTestRegistry()
|
||||
b := broker.NewHttpBroker(broker.Registry(m))
|
||||
|
||||
if err := b.Init(); err != nil {
|
||||
t.Fatalf("Unexpected init error: %v", err)
|
||||
}
|
||||
|
||||
if err := b.Connect(); err != nil {
|
||||
t.Fatalf("Unexpected connect error: %v", err)
|
||||
}
|
||||
|
||||
msg := &broker.Message{
|
||||
Header: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
Body: []byte(`{"message": "Hello World"}`),
|
||||
}
|
||||
|
||||
done := make(chan bool)
|
||||
|
||||
sub, err := b.Subscribe("test", func(p broker.Event) error {
|
||||
m := p.Message()
|
||||
|
||||
if string(m.Body) != string(msg.Body) {
|
||||
t.Fatalf("Unexpected msg %s, expected %s", string(m.Body), string(msg.Body))
|
||||
}
|
||||
|
||||
close(done)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected subscribe error: %v", err)
|
||||
}
|
||||
|
||||
if err := b.Publish("test", msg); err != nil {
|
||||
t.Fatalf("Unexpected publish error: %v", err)
|
||||
}
|
||||
|
||||
<-done
|
||||
if err := sub.Unsubscribe(); err != nil {
|
||||
t.Fatalf("Unexpected unsubscribe error: %v", err)
|
||||
}
|
||||
|
||||
if err := b.Disconnect(); err != nil {
|
||||
t.Fatalf("Unexpected disconnect error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentSubBroker(t *testing.T) {
|
||||
m := newTestRegistry()
|
||||
b := broker.NewHttpBroker(broker.Registry(m))
|
||||
|
||||
if err := b.Init(); err != nil {
|
||||
t.Fatalf("Unexpected init error: %v", err)
|
||||
}
|
||||
|
||||
if err := b.Connect(); err != nil {
|
||||
t.Fatalf("Unexpected connect error: %v", err)
|
||||
}
|
||||
|
||||
msg := &broker.Message{
|
||||
Header: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
Body: []byte(`{"message": "Hello World"}`),
|
||||
}
|
||||
|
||||
var subs []broker.Subscriber
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
sub, err := b.Subscribe("test", func(p broker.Event) error {
|
||||
defer wg.Done()
|
||||
|
||||
m := p.Message()
|
||||
|
||||
if string(m.Body) != string(msg.Body) {
|
||||
t.Fatalf("Unexpected msg %s, expected %s", string(m.Body), string(msg.Body))
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected subscribe error: %v", err)
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
subs = append(subs, sub)
|
||||
}
|
||||
|
||||
if err := b.Publish("test", msg); err != nil {
|
||||
t.Fatalf("Unexpected publish error: %v", err)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
for _, sub := range subs {
|
||||
if err := sub.Unsubscribe(); err != nil {
|
||||
t.Fatalf("Unexpected unsubscribe error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := b.Disconnect(); err != nil {
|
||||
t.Fatalf("Unexpected disconnect error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentPubBroker(t *testing.T) {
|
||||
m := newTestRegistry()
|
||||
b := broker.NewHttpBroker(broker.Registry(m))
|
||||
|
||||
if err := b.Init(); err != nil {
|
||||
t.Fatalf("Unexpected init error: %v", err)
|
||||
}
|
||||
|
||||
if err := b.Connect(); err != nil {
|
||||
t.Fatalf("Unexpected connect error: %v", err)
|
||||
}
|
||||
|
||||
msg := &broker.Message{
|
||||
Header: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
Body: []byte(`{"message": "Hello World"}`),
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
sub, err := b.Subscribe("test", func(p broker.Event) error {
|
||||
defer wg.Done()
|
||||
|
||||
m := p.Message()
|
||||
|
||||
if string(m.Body) != string(msg.Body) {
|
||||
t.Fatalf("Unexpected msg %s, expected %s", string(m.Body), string(msg.Body))
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected subscribe error: %v", err)
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
|
||||
if err := b.Publish("test", msg); err != nil {
|
||||
t.Fatalf("Unexpected publish error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if err := sub.Unsubscribe(); err != nil {
|
||||
t.Fatalf("Unexpected unsubscribe error: %v", err)
|
||||
}
|
||||
|
||||
if err := b.Disconnect(); err != nil {
|
||||
t.Fatalf("Unexpected disconnect error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSub1(b *testing.B) {
|
||||
sub(b, 1)
|
||||
}
|
||||
func BenchmarkSub8(b *testing.B) {
|
||||
sub(b, 8)
|
||||
}
|
||||
|
||||
func BenchmarkSub32(b *testing.B) {
|
||||
sub(b, 32)
|
||||
}
|
||||
|
||||
func BenchmarkPub1(b *testing.B) {
|
||||
pub(b, 1)
|
||||
}
|
||||
|
||||
func BenchmarkPub8(b *testing.B) {
|
||||
pub(b, 8)
|
||||
}
|
||||
|
||||
func BenchmarkPub32(b *testing.B) {
|
||||
pub(b, 32)
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
// Package memory provides a memory broker
|
||||
package broker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math/rand"
|
||||
"sync"
|
||||
|
||||
"github.com/google/uuid"
|
||||
maddr "go-micro.dev/v6/internal/util/addr"
|
||||
mnet "go-micro.dev/v6/internal/util/net"
|
||||
log "go-micro.dev/v6/logger"
|
||||
)
|
||||
|
||||
type memoryBroker struct {
|
||||
opts *Options
|
||||
|
||||
Subscribers map[string][]*memorySubscriber
|
||||
|
||||
addr string
|
||||
sync.RWMutex
|
||||
connected bool
|
||||
}
|
||||
|
||||
type memoryEvent struct {
|
||||
err error
|
||||
message interface{}
|
||||
opts *Options
|
||||
topic string
|
||||
}
|
||||
|
||||
type memorySubscriber struct {
|
||||
opts SubscribeOptions
|
||||
exit chan bool
|
||||
handler Handler
|
||||
id string
|
||||
topic string
|
||||
}
|
||||
|
||||
func (m *memoryBroker) Options() Options {
|
||||
return *m.opts
|
||||
}
|
||||
|
||||
func (m *memoryBroker) Address() string {
|
||||
return m.addr
|
||||
}
|
||||
|
||||
func (m *memoryBroker) Connect() error {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
if m.connected {
|
||||
return nil
|
||||
}
|
||||
|
||||
// use 127.0.0.1 to avoid scan of all network interfaces
|
||||
addr, err := maddr.Extract("127.0.0.1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i := rand.Intn(20000)
|
||||
// set addr with port
|
||||
addr = mnet.HostPort(addr, 10000+i)
|
||||
|
||||
m.addr = addr
|
||||
m.connected = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memoryBroker) Disconnect() error {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
if !m.connected {
|
||||
return nil
|
||||
}
|
||||
|
||||
m.connected = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memoryBroker) Init(opts ...Option) error {
|
||||
for _, o := range opts {
|
||||
o(m.opts)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memoryBroker) Publish(topic string, msg *Message, opts ...PublishOption) error {
|
||||
m.RLock()
|
||||
if !m.connected {
|
||||
m.RUnlock()
|
||||
return errors.New("not connected")
|
||||
}
|
||||
|
||||
subs, ok := m.Subscribers[topic]
|
||||
m.RUnlock()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
var v interface{}
|
||||
if m.opts.Codec != nil {
|
||||
buf, err := m.opts.Codec.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v = buf
|
||||
} else {
|
||||
v = msg
|
||||
}
|
||||
|
||||
p := &memoryEvent{
|
||||
topic: topic,
|
||||
message: v,
|
||||
opts: m.opts,
|
||||
}
|
||||
|
||||
for _, sub := range subs {
|
||||
if err := sub.handler(p); err != nil {
|
||||
p.err = err
|
||||
if eh := m.opts.ErrorHandler; eh != nil {
|
||||
_ = eh(p)
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memoryBroker) Subscribe(topic string, handler Handler, opts ...SubscribeOption) (Subscriber, error) {
|
||||
m.RLock()
|
||||
if !m.connected {
|
||||
m.RUnlock()
|
||||
return nil, errors.New("not connected")
|
||||
}
|
||||
m.RUnlock()
|
||||
|
||||
var options SubscribeOptions
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
sub := &memorySubscriber{
|
||||
exit: make(chan bool, 1),
|
||||
id: uuid.New().String(),
|
||||
topic: topic,
|
||||
handler: handler,
|
||||
opts: options,
|
||||
}
|
||||
|
||||
m.Lock()
|
||||
m.Subscribers[topic] = append(m.Subscribers[topic], sub)
|
||||
m.Unlock()
|
||||
|
||||
go func() {
|
||||
<-sub.exit
|
||||
m.Lock()
|
||||
var newSubscribers []*memorySubscriber
|
||||
for _, sb := range m.Subscribers[topic] {
|
||||
if sb.id == sub.id {
|
||||
continue
|
||||
}
|
||||
newSubscribers = append(newSubscribers, sb)
|
||||
}
|
||||
m.Subscribers[topic] = newSubscribers
|
||||
m.Unlock()
|
||||
}()
|
||||
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
func (m *memoryBroker) String() string {
|
||||
return "memory"
|
||||
}
|
||||
|
||||
func (m *memoryEvent) Topic() string {
|
||||
return m.topic
|
||||
}
|
||||
|
||||
func (m *memoryEvent) Message() *Message {
|
||||
switch v := m.message.(type) {
|
||||
case *Message:
|
||||
return v
|
||||
case []byte:
|
||||
msg := &Message{}
|
||||
if err := m.opts.Codec.Unmarshal(v, msg); err != nil {
|
||||
m.opts.Logger.Logf(log.ErrorLevel, "[memory]: failed to unmarshal: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memoryEvent) Ack() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memoryEvent) Error() error {
|
||||
return m.err
|
||||
}
|
||||
|
||||
func (m *memorySubscriber) Options() SubscribeOptions {
|
||||
return m.opts
|
||||
}
|
||||
|
||||
func (m *memorySubscriber) Topic() string {
|
||||
return m.topic
|
||||
}
|
||||
|
||||
func (m *memorySubscriber) Unsubscribe() error {
|
||||
m.exit <- true
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewMemoryBroker(opts ...Option) Broker {
|
||||
options := NewOptions(opts...)
|
||||
|
||||
return &memoryBroker{
|
||||
opts: options,
|
||||
Subscribers: make(map[string][]*memorySubscriber),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package broker_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"go-micro.dev/v6/broker"
|
||||
)
|
||||
|
||||
func TestMemoryBroker(t *testing.T) {
|
||||
b := broker.NewMemoryBroker()
|
||||
|
||||
if err := b.Connect(); err != nil {
|
||||
t.Fatalf("Unexpected connect error %v", err)
|
||||
}
|
||||
|
||||
topic := "test"
|
||||
count := 10
|
||||
|
||||
fn := func(p broker.Event) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
sub, err := b.Subscribe(topic, fn)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error subscribing %v", err)
|
||||
}
|
||||
|
||||
for i := 0; i < count; i++ {
|
||||
message := &broker.Message{
|
||||
Header: map[string]string{
|
||||
"foo": "bar",
|
||||
"id": fmt.Sprintf("%d", i),
|
||||
},
|
||||
Body: []byte(`hello world`),
|
||||
}
|
||||
|
||||
if err := b.Publish(topic, message); err != nil {
|
||||
t.Fatalf("Unexpected error publishing %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
if err := sub.Unsubscribe(); err != nil {
|
||||
t.Fatalf("Unexpected error unsubscribing from %s: %v", topic, err)
|
||||
}
|
||||
|
||||
if err := b.Disconnect(); err != nil {
|
||||
t.Fatalf("Unexpected connect error %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package nats
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go-micro.dev/v6/broker"
|
||||
)
|
||||
|
||||
// setBrokerOption returns a function to setup a context with given value.
|
||||
func setBrokerOption(k, v interface{}) broker.Option {
|
||||
return func(o *broker.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, k, v)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
// Package nats provides a NATS broker
|
||||
package nats
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
natsp "github.com/nats-io/nats.go"
|
||||
"go-micro.dev/v6/broker"
|
||||
"go-micro.dev/v6/codec/json"
|
||||
"go-micro.dev/v6/logger"
|
||||
"go-micro.dev/v6/registry"
|
||||
)
|
||||
|
||||
type natsBroker struct {
|
||||
sync.Once
|
||||
sync.RWMutex
|
||||
|
||||
// indicate if we're connected
|
||||
connected bool
|
||||
|
||||
addrs []string
|
||||
conn *natsp.Conn // single connection (used when pool is disabled)
|
||||
pool *connectionPool // connection pool (used when pooling is enabled)
|
||||
opts broker.Options
|
||||
nopts natsp.Options
|
||||
|
||||
// pool configuration
|
||||
poolSize int
|
||||
poolIdleTimeout time.Duration
|
||||
|
||||
// should we drain the connection
|
||||
drain bool
|
||||
closeCh chan (error)
|
||||
}
|
||||
|
||||
type subscriber struct {
|
||||
s *natsp.Subscription
|
||||
opts broker.SubscribeOptions
|
||||
}
|
||||
|
||||
type publication struct {
|
||||
t string
|
||||
err error
|
||||
m *broker.Message
|
||||
}
|
||||
|
||||
func (p *publication) Topic() string {
|
||||
return p.t
|
||||
}
|
||||
|
||||
func (p *publication) Message() *broker.Message {
|
||||
return p.m
|
||||
}
|
||||
|
||||
func (p *publication) Ack() error {
|
||||
// nats does not support acking
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *publication) Error() error {
|
||||
return p.err
|
||||
}
|
||||
|
||||
func (s *subscriber) Options() broker.SubscribeOptions {
|
||||
return s.opts
|
||||
}
|
||||
|
||||
func (s *subscriber) Topic() string {
|
||||
return s.s.Subject
|
||||
}
|
||||
|
||||
func (s *subscriber) Unsubscribe() error {
|
||||
return s.s.Unsubscribe()
|
||||
}
|
||||
|
||||
func (n *natsBroker) Address() string {
|
||||
if n.conn != nil && n.conn.IsConnected() {
|
||||
return n.conn.ConnectedUrl()
|
||||
}
|
||||
|
||||
if len(n.addrs) > 0 {
|
||||
return n.addrs[0]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (n *natsBroker) setAddrs(addrs []string) []string {
|
||||
//nolint:prealloc
|
||||
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{natsp.DefaultURL}
|
||||
}
|
||||
return cAddrs
|
||||
}
|
||||
|
||||
func (n *natsBroker) Connect() error {
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
|
||||
if n.connected {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if we should use connection pooling
|
||||
if n.poolSize > 1 {
|
||||
// Initialize connection pool
|
||||
factory := func() (*natsp.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 n.opts.TLSConfig != nil {
|
||||
opts.Secure = true
|
||||
}
|
||||
|
||||
return opts.Connect()
|
||||
}
|
||||
|
||||
pool, err := newConnectionPool(n.poolSize, factory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set idle timeout if configured
|
||||
if n.poolIdleTimeout > 0 {
|
||||
pool.idleTimeout = n.poolIdleTimeout
|
||||
}
|
||||
|
||||
n.pool = pool
|
||||
n.connected = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Single connection mode (original behavior)
|
||||
status := natsp.CLOSED
|
||||
if n.conn != nil {
|
||||
status = n.conn.Status()
|
||||
}
|
||||
|
||||
switch status {
|
||||
case natsp.CONNECTED, natsp.RECONNECTING, natsp.CONNECTING:
|
||||
n.connected = true
|
||||
return nil
|
||||
default: // DISCONNECTED or CLOSED or DRAINING
|
||||
opts := n.nopts
|
||||
opts.Servers = n.addrs
|
||||
opts.Secure = n.opts.Secure
|
||||
opts.TLSConfig = n.opts.TLSConfig
|
||||
|
||||
// secure might not be set
|
||||
if n.opts.TLSConfig != nil {
|
||||
opts.Secure = true
|
||||
}
|
||||
|
||||
c, err := opts.Connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n.conn = c
|
||||
n.connected = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (n *natsBroker) Disconnect() error {
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
|
||||
// Close connection pool if it exists
|
||||
if n.pool != nil {
|
||||
if err := n.pool.Close(); err != nil {
|
||||
n.opts.Logger.Log(logger.ErrorLevel, "error closing connection pool:", err)
|
||||
}
|
||||
n.pool = nil
|
||||
}
|
||||
|
||||
// Close single connection if it exists
|
||||
if n.conn != nil {
|
||||
// drain the connection if specified
|
||||
if n.drain {
|
||||
_ = n.conn.Drain()
|
||||
n.closeCh <- nil
|
||||
}
|
||||
|
||||
// close the client connection
|
||||
n.conn.Close()
|
||||
n.conn = nil
|
||||
}
|
||||
|
||||
// set not connected
|
||||
n.connected = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *natsBroker) Init(opts ...broker.Option) error {
|
||||
n.setOption(opts...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *natsBroker) Options() broker.Options {
|
||||
return n.opts
|
||||
}
|
||||
|
||||
func (n *natsBroker) Publish(topic string, msg *broker.Message, opts ...broker.PublishOption) error {
|
||||
n.RLock()
|
||||
defer n.RUnlock()
|
||||
|
||||
b, err := n.opts.Codec.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Use connection pool if enabled
|
||||
if n.pool != nil {
|
||||
poolConn, err := n.pool.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = n.pool.Put(poolConn) }()
|
||||
|
||||
conn := poolConn.Conn()
|
||||
if conn == nil {
|
||||
return errors.New("invalid connection from pool")
|
||||
}
|
||||
return conn.Publish(topic, b)
|
||||
}
|
||||
|
||||
// Use single connection (original behavior)
|
||||
if n.conn == nil {
|
||||
return errors.New("not connected")
|
||||
}
|
||||
|
||||
return n.conn.Publish(topic, b)
|
||||
}
|
||||
|
||||
func (n *natsBroker) Subscribe(topic string, handler broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
|
||||
n.RLock()
|
||||
hasConnection := n.conn != nil || n.pool != nil
|
||||
n.RUnlock()
|
||||
|
||||
if !hasConnection {
|
||||
return nil, errors.New("not connected")
|
||||
}
|
||||
|
||||
opt := broker.SubscribeOptions{
|
||||
AutoAck: true,
|
||||
Context: context.Background(),
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
fn := func(msg *natsp.Msg) {
|
||||
var m broker.Message
|
||||
pub := &publication{t: msg.Subject}
|
||||
eh := n.opts.ErrorHandler
|
||||
err := n.opts.Codec.Unmarshal(msg.Data, &m)
|
||||
pub.err = err
|
||||
pub.m = &m
|
||||
if err != nil {
|
||||
m.Body = msg.Data
|
||||
n.opts.Logger.Log(logger.ErrorLevel, err)
|
||||
if eh != nil {
|
||||
_ = eh(pub)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err := handler(pub); err != nil {
|
||||
pub.err = err
|
||||
n.opts.Logger.Log(logger.ErrorLevel, err)
|
||||
if eh != nil {
|
||||
_ = eh(pub)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sub *natsp.Subscription
|
||||
var err error
|
||||
|
||||
// Use connection pool if enabled
|
||||
if n.pool != nil {
|
||||
poolConn, err := n.pool.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn := poolConn.Conn()
|
||||
if conn == nil {
|
||||
_ = n.pool.Put(poolConn)
|
||||
return nil, errors.New("invalid connection from pool")
|
||||
}
|
||||
|
||||
if len(opt.Queue) > 0 {
|
||||
sub, err = conn.QueueSubscribe(topic, opt.Queue, fn)
|
||||
} else {
|
||||
sub, err = conn.Subscribe(topic, fn)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
_ = n.pool.Put(poolConn)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return connection to pool after subscription is created
|
||||
// The subscription keeps the connection alive
|
||||
_ = n.pool.Put(poolConn)
|
||||
|
||||
return &subscriber{s: sub, opts: opt}, nil
|
||||
}
|
||||
|
||||
// Use single connection (original behavior)
|
||||
n.RLock()
|
||||
if len(opt.Queue) > 0 {
|
||||
sub, err = n.conn.QueueSubscribe(topic, opt.Queue, fn)
|
||||
} else {
|
||||
sub, err = n.conn.Subscribe(topic, fn)
|
||||
}
|
||||
n.RUnlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &subscriber{s: sub, opts: opt}, nil
|
||||
}
|
||||
|
||||
func (n *natsBroker) String() string {
|
||||
return "nats"
|
||||
}
|
||||
|
||||
func (n *natsBroker) setOption(opts ...broker.Option) {
|
||||
for _, o := range opts {
|
||||
o(&n.opts)
|
||||
}
|
||||
|
||||
n.Do(func() {
|
||||
n.nopts = natsp.GetDefaultOptions()
|
||||
n.poolSize = 1 // Default to single connection (no pooling)
|
||||
n.poolIdleTimeout = 5 * time.Minute
|
||||
})
|
||||
|
||||
if nopts, ok := n.opts.Context.Value(optionsKey{}).(natsp.Options); ok {
|
||||
n.nopts = nopts
|
||||
}
|
||||
|
||||
// Set pool size if configured
|
||||
if poolSize, ok := n.opts.Context.Value(poolSizeKey{}).(int); ok && poolSize > 0 {
|
||||
n.poolSize = poolSize
|
||||
}
|
||||
|
||||
// Set pool idle timeout if configured
|
||||
if idleTimeout, ok := n.opts.Context.Value(poolIdleTimeoutKey{}).(time.Duration); ok {
|
||||
n.poolIdleTimeout = idleTimeout
|
||||
}
|
||||
|
||||
// broker.Options have higher priority than nats.Options
|
||||
// only if Addrs, Secure or TLSConfig were not set through a broker.Option
|
||||
// we read them from nats.Option
|
||||
if len(n.opts.Addrs) == 0 {
|
||||
n.opts.Addrs = n.nopts.Servers
|
||||
}
|
||||
|
||||
if !n.opts.Secure {
|
||||
n.opts.Secure = n.nopts.Secure
|
||||
}
|
||||
|
||||
if n.opts.TLSConfig == nil {
|
||||
n.opts.TLSConfig = n.nopts.TLSConfig
|
||||
}
|
||||
n.addrs = n.setAddrs(n.opts.Addrs)
|
||||
|
||||
if n.opts.Context.Value(drainConnectionKey{}) != nil {
|
||||
n.drain = true
|
||||
n.closeCh = make(chan error)
|
||||
n.nopts.ClosedCB = n.onClose
|
||||
n.nopts.AsyncErrorCB = n.onAsyncError
|
||||
n.nopts.DisconnectedErrCB = n.onDisconnectedError
|
||||
}
|
||||
}
|
||||
|
||||
func (n *natsBroker) onClose(conn *natsp.Conn) {
|
||||
n.closeCh <- nil
|
||||
}
|
||||
|
||||
func (n *natsBroker) onAsyncError(conn *natsp.Conn, sub *natsp.Subscription, err error) {
|
||||
// There are kinds of different async error nats might callback, but we are interested
|
||||
// in ErrDrainTimeout only here.
|
||||
if err == natsp.ErrDrainTimeout {
|
||||
n.closeCh <- err
|
||||
}
|
||||
}
|
||||
|
||||
func (n *natsBroker) onDisconnectedError(conn *natsp.Conn, err error) {
|
||||
n.closeCh <- err
|
||||
}
|
||||
|
||||
func NewNatsBroker(opts ...broker.Option) broker.Broker {
|
||||
options := broker.Options{
|
||||
// Default codec
|
||||
Codec: json.Marshaler{},
|
||||
Context: context.Background(),
|
||||
Registry: registry.DefaultRegistry,
|
||||
Logger: logger.DefaultLogger,
|
||||
}
|
||||
|
||||
n := &natsBroker{
|
||||
opts: options,
|
||||
}
|
||||
n.setOption(opts...)
|
||||
|
||||
return n
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package nats
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
natsp "github.com/nats-io/nats.go"
|
||||
"go-micro.dev/v6/broker"
|
||||
)
|
||||
|
||||
var addrTestCases = []struct {
|
||||
name string
|
||||
description string
|
||||
addrs map[string]string // expected address : set address
|
||||
}{
|
||||
{
|
||||
"brokerOpts",
|
||||
"set broker addresses through a broker.Option in constructor",
|
||||
map[string]string{
|
||||
"nats://192.168.10.1:5222": "192.168.10.1:5222",
|
||||
"nats://10.20.10.0:4222": "10.20.10.0:4222"},
|
||||
},
|
||||
{
|
||||
"brokerInit",
|
||||
"set broker addresses through a broker.Option in broker.Init()",
|
||||
map[string]string{
|
||||
"nats://192.168.10.1:5222": "192.168.10.1:5222",
|
||||
"nats://10.20.10.0:4222": "10.20.10.0:4222"},
|
||||
},
|
||||
{
|
||||
"natsOpts",
|
||||
"set broker addresses through the nats.Option in constructor",
|
||||
map[string]string{
|
||||
"nats://192.168.10.1:5222": "192.168.10.1:5222",
|
||||
"nats://10.20.10.0:4222": "10.20.10.0:4222"},
|
||||
},
|
||||
{
|
||||
"default",
|
||||
"check if default Address is set correctly",
|
||||
map[string]string{
|
||||
"nats://127.0.0.1:4222": "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// TestInitAddrs tests issue #100. Ensures that if the addrs is set by an option in init it will be used.
|
||||
func TestInitAddrs(t *testing.T) {
|
||||
for _, tc := range addrTestCases {
|
||||
t.Run(fmt.Sprintf("%s: %s", tc.name, tc.description), func(t *testing.T) {
|
||||
var br broker.Broker
|
||||
var addrs []string
|
||||
|
||||
for _, addr := range tc.addrs {
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
|
||||
switch tc.name {
|
||||
case "brokerOpts":
|
||||
// we know that there are just two addrs in the dict
|
||||
br = NewNatsBroker(broker.Addrs(addrs[0], addrs[1]))
|
||||
br.Init()
|
||||
case "brokerInit":
|
||||
br = NewNatsBroker()
|
||||
// we know that there are just two addrs in the dict
|
||||
br.Init(broker.Addrs(addrs[0], addrs[1]))
|
||||
case "natsOpts":
|
||||
nopts := natsp.GetDefaultOptions()
|
||||
nopts.Servers = addrs
|
||||
br = NewNatsBroker(Options(nopts))
|
||||
br.Init()
|
||||
case "default":
|
||||
br = NewNatsBroker()
|
||||
br.Init()
|
||||
}
|
||||
|
||||
natsBroker, ok := br.(*natsBroker)
|
||||
if !ok {
|
||||
t.Fatal("Expected broker to be of types *natsBroker")
|
||||
}
|
||||
// check if the same amount of addrs we set has actually been set, default
|
||||
// have only 1 address nats://127.0.0.1:4222 (current nats code) or
|
||||
// nats://localhost:4222 (older code version)
|
||||
if len(natsBroker.addrs) != len(tc.addrs) && tc.name != "default" {
|
||||
t.Errorf("Expected Addr count = %d, Actual Addr count = %d",
|
||||
len(natsBroker.addrs), len(tc.addrs))
|
||||
}
|
||||
|
||||
for _, addr := range natsBroker.addrs {
|
||||
_, ok := tc.addrs[addr]
|
||||
if !ok {
|
||||
t.Errorf("Expected '%s' has not been set", addr)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package nats
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
natsp "github.com/nats-io/nats.go"
|
||||
"go-micro.dev/v6/broker"
|
||||
)
|
||||
|
||||
type optionsKey struct{}
|
||||
type drainConnectionKey struct{}
|
||||
type poolSizeKey struct{}
|
||||
type poolIdleTimeoutKey struct{}
|
||||
|
||||
// Options accepts nats.Options.
|
||||
func Options(opts natsp.Options) broker.Option {
|
||||
return setBrokerOption(optionsKey{}, opts)
|
||||
}
|
||||
|
||||
// DrainConnection will drain subscription on close.
|
||||
func DrainConnection() broker.Option {
|
||||
return setBrokerOption(drainConnectionKey{}, struct{}{})
|
||||
}
|
||||
|
||||
// PoolSize sets the size of the connection pool.
|
||||
// If set to a value > 1, the broker will use a connection pool.
|
||||
// Default is 1 (no pooling).
|
||||
func PoolSize(size int) broker.Option {
|
||||
return setBrokerOption(poolSizeKey{}, size)
|
||||
}
|
||||
|
||||
// PoolIdleTimeout sets the timeout for idle connections in the pool.
|
||||
// Connections idle for longer than this duration will be closed.
|
||||
// Default is 5 minutes. Set to 0 to disable idle timeout.
|
||||
func PoolIdleTimeout(timeout time.Duration) broker.Option {
|
||||
return setBrokerOption(poolIdleTimeoutKey{}, timeout)
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package nats
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
natsp "github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrPoolExhausted is returned when no connections are available in the pool
|
||||
ErrPoolExhausted = errors.New("connection pool exhausted")
|
||||
// ErrPoolClosed is returned when trying to use a closed pool
|
||||
ErrPoolClosed = errors.New("connection pool is closed")
|
||||
)
|
||||
|
||||
// connectionPool manages a pool of NATS connections
|
||||
type connectionPool struct {
|
||||
mu sync.RWMutex
|
||||
connections chan *pooledConnection
|
||||
factory func() (*natsp.Conn, error)
|
||||
size int
|
||||
idleTimeout time.Duration
|
||||
closed bool
|
||||
}
|
||||
|
||||
// pooledConnection wraps a NATS connection with metadata
|
||||
type pooledConnection struct {
|
||||
conn *natsp.Conn
|
||||
createdAt time.Time
|
||||
lastUsed time.Time
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// newConnectionPool creates a new connection pool
|
||||
func newConnectionPool(size int, factory func() (*natsp.Conn, error)) (*connectionPool, error) {
|
||||
if size <= 0 {
|
||||
size = 1
|
||||
}
|
||||
|
||||
pool := &connectionPool{
|
||||
connections: make(chan *pooledConnection, size),
|
||||
factory: factory,
|
||||
size: size,
|
||||
idleTimeout: 5 * time.Minute,
|
||||
closed: false,
|
||||
}
|
||||
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
// Get retrieves a connection from the pool or creates a new one
|
||||
func (p *connectionPool) Get() (*pooledConnection, error) {
|
||||
p.mu.RLock()
|
||||
if p.closed {
|
||||
p.mu.RUnlock()
|
||||
return nil, ErrPoolClosed
|
||||
}
|
||||
p.mu.RUnlock()
|
||||
|
||||
// Try to get an existing connection from the pool
|
||||
select {
|
||||
case conn := <-p.connections:
|
||||
// Check if connection is still valid and not idle for too long
|
||||
if conn.isValid() && !conn.isExpired(p.idleTimeout) {
|
||||
conn.updateLastUsed()
|
||||
return conn, nil
|
||||
}
|
||||
// Connection is invalid or expired, close it and create a new one
|
||||
conn.close()
|
||||
return p.createConnection()
|
||||
default:
|
||||
// No connection available, create a new one
|
||||
return p.createConnection()
|
||||
}
|
||||
}
|
||||
|
||||
// Put returns a connection to the pool
|
||||
func (p *connectionPool) Put(conn *pooledConnection) error {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
||||
if p.closed {
|
||||
return conn.close()
|
||||
}
|
||||
|
||||
// Check if connection is still valid
|
||||
if !conn.isValid() {
|
||||
return conn.close()
|
||||
}
|
||||
|
||||
conn.updateLastUsed()
|
||||
|
||||
// Try to return connection to pool
|
||||
select {
|
||||
case p.connections <- conn:
|
||||
return nil
|
||||
default:
|
||||
// Pool is full, close the connection
|
||||
return conn.close()
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes all connections in the pool
|
||||
func (p *connectionPool) Close() error {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.closed {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.closed = true
|
||||
close(p.connections)
|
||||
|
||||
// Close all connections in the pool
|
||||
for conn := range p.connections {
|
||||
conn.close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createConnection creates a new pooled connection
|
||||
func (p *connectionPool) createConnection() (*pooledConnection, error) {
|
||||
conn, err := p.factory()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pooledConnection{
|
||||
conn: conn,
|
||||
createdAt: time.Now(),
|
||||
lastUsed: time.Now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// isValid checks if the underlying NATS connection is valid
|
||||
func (pc *pooledConnection) isValid() bool {
|
||||
pc.mu.Lock()
|
||||
defer pc.mu.Unlock()
|
||||
|
||||
if pc.conn == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
status := pc.conn.Status()
|
||||
return status == natsp.CONNECTED || status == natsp.RECONNECTING
|
||||
}
|
||||
|
||||
// isExpired checks if the connection has been idle for too long
|
||||
func (pc *pooledConnection) isExpired(timeout time.Duration) bool {
|
||||
pc.mu.Lock()
|
||||
defer pc.mu.Unlock()
|
||||
|
||||
if timeout <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return time.Since(pc.lastUsed) > timeout
|
||||
}
|
||||
|
||||
// close closes the underlying NATS connection
|
||||
func (pc *pooledConnection) close() error {
|
||||
pc.mu.Lock()
|
||||
defer pc.mu.Unlock()
|
||||
|
||||
if pc.conn != nil {
|
||||
pc.conn.Close()
|
||||
pc.conn = nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Conn returns the underlying NATS connection
|
||||
func (pc *pooledConnection) Conn() *natsp.Conn {
|
||||
pc.mu.Lock()
|
||||
defer pc.mu.Unlock()
|
||||
return pc.conn
|
||||
}
|
||||
|
||||
// updateLastUsed updates the last used timestamp in a thread-safe manner
|
||||
func (pc *pooledConnection) updateLastUsed() {
|
||||
pc.mu.Lock()
|
||||
defer pc.mu.Unlock()
|
||||
pc.lastUsed = time.Now()
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package nats
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
natsp "github.com/nats-io/nats.go"
|
||||
)
|
||||
|
||||
func TestConnectionPool_GetPut(t *testing.T) {
|
||||
// Mock factory that creates connections
|
||||
connCount := 0
|
||||
factory := func() (*natsp.Conn, error) {
|
||||
connCount++
|
||||
// Return a mock connection (we can't create real NATS connections in tests without a server)
|
||||
// This test is more about the pool logic
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
pool, err := newConnectionPool(3, factory)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create pool: %v", err)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
// Get a connection (should create one)
|
||||
conn1, err := pool.Get()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get connection: %v", err)
|
||||
}
|
||||
if conn1 == nil {
|
||||
t.Fatal("Expected connection, got nil")
|
||||
}
|
||||
|
||||
// Put it back
|
||||
if err := pool.Put(conn1); err != nil {
|
||||
t.Fatalf("Failed to put connection: %v", err)
|
||||
}
|
||||
|
||||
// Get it again (should reuse the same one)
|
||||
conn2, err := pool.Get()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get connection: %v", err)
|
||||
}
|
||||
|
||||
// Since we can't compare actual connections easily, just verify we got one
|
||||
if conn2 == nil {
|
||||
t.Fatal("Expected connection, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnectionPool_Concurrent(t *testing.T) {
|
||||
connCount := 0
|
||||
mu := sync.Mutex{}
|
||||
factory := func() (*natsp.Conn, error) {
|
||||
mu.Lock()
|
||||
connCount++
|
||||
mu.Unlock()
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
pool, err := newConnectionPool(5, factory)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create pool: %v", err)
|
||||
}
|
||||
defer pool.Close()
|
||||
|
||||
// Simulate concurrent access
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 10; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
conn, err := pool.Get()
|
||||
if err != nil {
|
||||
t.Errorf("Failed to get connection: %v", err)
|
||||
return
|
||||
}
|
||||
// Simulate some work
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
if err := pool.Put(conn); err != nil {
|
||||
t.Errorf("Failed to put connection: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// We should have created some connections
|
||||
mu.Lock()
|
||||
if connCount == 0 {
|
||||
t.Error("Expected at least one connection to be created")
|
||||
}
|
||||
mu.Unlock()
|
||||
}
|
||||
|
||||
func TestConnectionPool_Close(t *testing.T) {
|
||||
factory := func() (*natsp.Conn, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
pool, err := newConnectionPool(3, factory)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create pool: %v", err)
|
||||
}
|
||||
|
||||
// Get a connection
|
||||
conn, err := pool.Get()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get connection: %v", err)
|
||||
}
|
||||
|
||||
// Close the pool
|
||||
if err := pool.Close(); err != nil {
|
||||
t.Fatalf("Failed to close pool: %v", err)
|
||||
}
|
||||
|
||||
// Put connection back to closed pool should not panic
|
||||
// The connection will be closed instead of returned to pool
|
||||
_ = pool.Put(conn)
|
||||
|
||||
// Try to get from closed pool
|
||||
_, err = pool.Get()
|
||||
if err != ErrPoolClosed {
|
||||
t.Errorf("Expected ErrPoolClosed, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPooledConnection_IsValid(t *testing.T) {
|
||||
pc := &pooledConnection{
|
||||
conn: nil, // nil connection should be invalid
|
||||
createdAt: time.Now(),
|
||||
lastUsed: time.Now(),
|
||||
}
|
||||
|
||||
if pc.isValid() {
|
||||
t.Error("Expected nil connection to be invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPooledConnection_IsExpired(t *testing.T) {
|
||||
pc := &pooledConnection{
|
||||
conn: nil,
|
||||
createdAt: time.Now(),
|
||||
lastUsed: time.Now().Add(-10 * time.Minute), // 10 minutes ago
|
||||
}
|
||||
|
||||
// With 5 minute timeout, should be expired
|
||||
if !pc.isExpired(5 * time.Minute) {
|
||||
t.Error("Expected connection to be expired")
|
||||
}
|
||||
|
||||
// With 0 timeout, should never expire
|
||||
if pc.isExpired(0) {
|
||||
t.Error("Expected connection not to expire with 0 timeout")
|
||||
}
|
||||
|
||||
// With 20 minute timeout, should not be expired
|
||||
if pc.isExpired(20 * time.Minute) {
|
||||
t.Error("Expected connection not to be expired")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNatsBroker_PoolConfiguration(t *testing.T) {
|
||||
// Test that pool size is set correctly
|
||||
br := NewNatsBroker(PoolSize(5))
|
||||
nb, ok := br.(*natsBroker)
|
||||
if !ok {
|
||||
t.Fatal("Expected broker to be of type *natsBroker")
|
||||
}
|
||||
|
||||
if nb.poolSize != 5 {
|
||||
t.Errorf("Expected pool size 5, got %d", nb.poolSize)
|
||||
}
|
||||
|
||||
// Test with custom idle timeout
|
||||
br2 := NewNatsBroker(PoolSize(3), PoolIdleTimeout(10*time.Minute))
|
||||
nb2, ok := br2.(*natsBroker)
|
||||
if !ok {
|
||||
t.Fatal("Expected broker to be of type *natsBroker")
|
||||
}
|
||||
|
||||
if nb2.poolSize != 3 {
|
||||
t.Errorf("Expected pool size 3, got %d", nb2.poolSize)
|
||||
}
|
||||
|
||||
if nb2.poolIdleTimeout != 10*time.Minute {
|
||||
t.Errorf("Expected idle timeout 10m, got %v", nb2.poolIdleTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNatsBroker_DefaultSingleConnection(t *testing.T) {
|
||||
// Test that default behavior is single connection (pool size 1)
|
||||
br := NewNatsBroker()
|
||||
nb, ok := br.(*natsBroker)
|
||||
if !ok {
|
||||
t.Fatal("Expected broker to be of type *natsBroker")
|
||||
}
|
||||
|
||||
if nb.poolSize != 1 {
|
||||
t.Errorf("Expected default pool size 1, got %d", nb.poolSize)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package broker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
|
||||
"go-micro.dev/v6/codec"
|
||||
"go-micro.dev/v6/logger"
|
||||
"go-micro.dev/v6/registry"
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
Codec codec.Marshaler
|
||||
|
||||
// Logger is the underlying logger
|
||||
Logger logger.Logger
|
||||
|
||||
// Registry used for clustering
|
||||
Registry registry.Registry
|
||||
// Other options for implementations of the interface
|
||||
// can be stored in a context
|
||||
Context context.Context
|
||||
|
||||
// Handler executed when error happens in broker mesage
|
||||
// processing
|
||||
ErrorHandler Handler
|
||||
|
||||
TLSConfig *tls.Config
|
||||
Addrs []string
|
||||
Secure bool
|
||||
}
|
||||
|
||||
type PublishOptions struct {
|
||||
// Other options for implementations of the interface
|
||||
// can be stored in a context
|
||||
Context context.Context
|
||||
}
|
||||
|
||||
type SubscribeOptions struct {
|
||||
|
||||
// Other options for implementations of the interface
|
||||
// can be stored in a context
|
||||
Context context.Context
|
||||
// Subscribers with the same queue name
|
||||
// will create a shared subscription where each
|
||||
// receives a subset of messages.
|
||||
Queue string
|
||||
|
||||
// AutoAck defaults to true. When a handler returns
|
||||
// with a nil error the message is acked.
|
||||
AutoAck bool
|
||||
}
|
||||
|
||||
type Option func(*Options)
|
||||
|
||||
type PublishOption func(*PublishOptions)
|
||||
|
||||
// PublishContext set context.
|
||||
func PublishContext(ctx context.Context) PublishOption {
|
||||
return func(o *PublishOptions) {
|
||||
o.Context = ctx
|
||||
}
|
||||
}
|
||||
|
||||
type SubscribeOption func(*SubscribeOptions)
|
||||
|
||||
func NewOptions(opts ...Option) *Options {
|
||||
options := Options{
|
||||
Context: context.Background(),
|
||||
Logger: logger.DefaultLogger,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
return &options
|
||||
}
|
||||
|
||||
func NewSubscribeOptions(opts ...SubscribeOption) SubscribeOptions {
|
||||
opt := SubscribeOptions{
|
||||
AutoAck: true,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
return opt
|
||||
}
|
||||
|
||||
// Addrs sets the host addresses to be used by the broker.
|
||||
func Addrs(addrs ...string) Option {
|
||||
return func(o *Options) {
|
||||
o.Addrs = addrs
|
||||
}
|
||||
}
|
||||
|
||||
// Codec sets the codec used for encoding/decoding used where
|
||||
// a broker does not support headers.
|
||||
func Codec(c codec.Marshaler) Option {
|
||||
return func(o *Options) {
|
||||
o.Codec = c
|
||||
}
|
||||
}
|
||||
|
||||
// DisableAutoAck will disable auto acking of messages
|
||||
// after they have been handled.
|
||||
func DisableAutoAck() SubscribeOption {
|
||||
return func(o *SubscribeOptions) {
|
||||
o.AutoAck = false
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorHandler will catch all broker errors that cant be handled
|
||||
// in normal way, for example Codec errors.
|
||||
func ErrorHandler(h Handler) Option {
|
||||
return func(o *Options) {
|
||||
o.ErrorHandler = h
|
||||
}
|
||||
}
|
||||
|
||||
// Queue sets the name of the queue to share messages on.
|
||||
func Queue(name string) SubscribeOption {
|
||||
return func(o *SubscribeOptions) {
|
||||
o.Queue = name
|
||||
}
|
||||
}
|
||||
|
||||
func Registry(r registry.Registry) Option {
|
||||
return func(o *Options) {
|
||||
o.Registry = r
|
||||
}
|
||||
}
|
||||
|
||||
// Secure communication with the broker.
|
||||
func Secure(b bool) Option {
|
||||
return func(o *Options) {
|
||||
o.Secure = b
|
||||
}
|
||||
}
|
||||
|
||||
// Specify TLS Config.
|
||||
func TLSConfig(t *tls.Config) Option {
|
||||
return func(o *Options) {
|
||||
o.TLSConfig = t
|
||||
}
|
||||
}
|
||||
|
||||
// Logger sets the underline logger.
|
||||
func Logger(l logger.Logger) Option {
|
||||
return func(o *Options) {
|
||||
o.Logger = l
|
||||
}
|
||||
}
|
||||
|
||||
// SubscribeContext set context.
|
||||
func SubscribeContext(ctx context.Context) SubscribeOption {
|
||||
return func(o *SubscribeOptions) {
|
||||
o.Context = ctx
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package rabbitmq
|
||||
|
||||
type ExternalAuthentication struct {
|
||||
}
|
||||
|
||||
func (auth *ExternalAuthentication) Mechanism() string {
|
||||
return "EXTERNAL"
|
||||
}
|
||||
|
||||
func (auth *ExternalAuthentication) Response() string {
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package rabbitmq
|
||||
|
||||
//
|
||||
// All credit to Mondo
|
||||
//
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/google/uuid"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
)
|
||||
|
||||
type rabbitMQChannel struct {
|
||||
uuid string
|
||||
connection *amqp.Connection
|
||||
channel *amqp.Channel
|
||||
confirmPublish chan amqp.Confirmation
|
||||
mtx sync.Mutex
|
||||
}
|
||||
|
||||
func newRabbitChannel(conn *amqp.Connection, prefetchCount int, prefetchGlobal bool, confirmPublish bool) (*rabbitMQChannel, error) {
|
||||
id, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rabbitCh := &rabbitMQChannel{
|
||||
uuid: id.String(),
|
||||
connection: conn,
|
||||
}
|
||||
if err := rabbitCh.Connect(prefetchCount, prefetchGlobal, confirmPublish); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rabbitCh, nil
|
||||
}
|
||||
|
||||
func (r *rabbitMQChannel) Connect(prefetchCount int, prefetchGlobal bool, confirmPublish bool) error {
|
||||
var err error
|
||||
r.channel, err = r.connection.Channel()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.channel.Qos(prefetchCount, 0, prefetchGlobal)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if confirmPublish {
|
||||
r.confirmPublish = r.channel.NotifyPublish(make(chan amqp.Confirmation, 1))
|
||||
|
||||
err = r.channel.Confirm(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rabbitMQChannel) Close() error {
|
||||
if r.channel == nil {
|
||||
return errors.New("channel is nil")
|
||||
}
|
||||
return r.channel.Close()
|
||||
}
|
||||
|
||||
func (r *rabbitMQChannel) Publish(exchange, key string, message amqp.Publishing) error {
|
||||
if r.channel == nil {
|
||||
return errors.New("channel is nil")
|
||||
}
|
||||
|
||||
if r.confirmPublish != nil {
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
}
|
||||
|
||||
err := r.channel.Publish(exchange, key, false, false, message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.confirmPublish != nil {
|
||||
confirmation, ok := <-r.confirmPublish
|
||||
if !ok {
|
||||
return errors.New("channel closed before could receive confirmation of publish")
|
||||
}
|
||||
|
||||
if !confirmation.Ack {
|
||||
return errors.New("could not publish message, received nack from broker on confirmation")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rabbitMQChannel) DeclareExchange(ex Exchange) error {
|
||||
return r.channel.ExchangeDeclare(
|
||||
ex.Name, // name
|
||||
string(ex.Type), // kind
|
||||
ex.Durable, // durable
|
||||
false, // autoDelete
|
||||
false, // internal
|
||||
false, // noWait
|
||||
nil, // args
|
||||
)
|
||||
}
|
||||
|
||||
func (r *rabbitMQChannel) DeclareDurableExchange(ex Exchange) error {
|
||||
return r.channel.ExchangeDeclare(
|
||||
ex.Name, // name
|
||||
string(ex.Type), // kind
|
||||
true, // durable
|
||||
false, // autoDelete
|
||||
false, // internal
|
||||
false, // noWait
|
||||
nil, // args
|
||||
)
|
||||
}
|
||||
|
||||
func (r *rabbitMQChannel) DeclareQueue(queue string, args amqp.Table) error {
|
||||
_, err := r.channel.QueueDeclare(
|
||||
queue, // name
|
||||
false, // durable
|
||||
true, // autoDelete
|
||||
false, // exclusive
|
||||
false, // noWait
|
||||
args, // args
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *rabbitMQChannel) DeclareDurableQueue(queue string, args amqp.Table) error {
|
||||
_, err := r.channel.QueueDeclare(
|
||||
queue, // name
|
||||
true, // durable
|
||||
false, // autoDelete
|
||||
false, // exclusive
|
||||
false, // noWait
|
||||
args, // args
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *rabbitMQChannel) DeclareReplyQueue(queue string) error {
|
||||
_, err := r.channel.QueueDeclare(
|
||||
queue, // name
|
||||
false, // durable
|
||||
true, // autoDelete
|
||||
true, // exclusive
|
||||
false, // noWait
|
||||
nil, // args
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *rabbitMQChannel) ConsumeQueue(queue string, autoAck bool) (<-chan amqp.Delivery, error) {
|
||||
return r.channel.Consume(
|
||||
queue, // queue
|
||||
r.uuid, // consumer
|
||||
autoAck, // autoAck
|
||||
false, // exclusive
|
||||
false, // nolocal
|
||||
false, // nowait
|
||||
nil, // args
|
||||
)
|
||||
}
|
||||
|
||||
func (r *rabbitMQChannel) BindQueue(queue, key, exchange string, args amqp.Table) error {
|
||||
return r.channel.QueueBind(
|
||||
queue, // name
|
||||
key, // key
|
||||
exchange, // exchange
|
||||
false, // noWait
|
||||
args, // args
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package rabbitmq
|
||||
|
||||
//
|
||||
// All credit to Mondo
|
||||
//
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
mtls "go-micro.dev/v6/internal/util/tls"
|
||||
"go-micro.dev/v6/logger"
|
||||
)
|
||||
|
||||
type MQExchangeType string
|
||||
|
||||
const (
|
||||
ExchangeTypeFanout MQExchangeType = "fanout"
|
||||
ExchangeTypeTopic MQExchangeType = "topic"
|
||||
ExchangeTypeDirect MQExchangeType = "direct"
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultExchange = Exchange{
|
||||
Name: "micro",
|
||||
Type: ExchangeTypeTopic,
|
||||
}
|
||||
DefaultRabbitURL = "amqp://guest:guest@127.0.0.1:5672"
|
||||
DefaultPrefetchCount = 0
|
||||
DefaultPrefetchGlobal = false
|
||||
DefaultRequeueOnError = false
|
||||
DefaultConfirmPublish = false
|
||||
DefaultWithoutExchange = false
|
||||
|
||||
// The amqp library does not seem to set these when using amqp.DialConfig
|
||||
// (even though it says so in the comments) so we set them manually to make
|
||||
// sure to not brake any existing functionality.
|
||||
defaultHeartbeat = 10 * time.Second
|
||||
defaultLocale = "en_US"
|
||||
|
||||
defaultAmqpConfig = amqp.Config{
|
||||
Heartbeat: defaultHeartbeat,
|
||||
Locale: defaultLocale,
|
||||
}
|
||||
|
||||
dialConfig = amqp.DialConfig
|
||||
)
|
||||
|
||||
type rabbitMQConn struct {
|
||||
Connection *amqp.Connection
|
||||
Channel *rabbitMQChannel
|
||||
ExchangeChannel *rabbitMQChannel
|
||||
exchange Exchange
|
||||
withoutExchange bool
|
||||
url string
|
||||
prefetchCount int
|
||||
prefetchGlobal bool
|
||||
confirmPublish bool
|
||||
|
||||
sync.Mutex
|
||||
connected bool
|
||||
close chan bool
|
||||
|
||||
waitConnection chan struct{}
|
||||
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// Exchange is the rabbitmq exchange.
|
||||
type Exchange struct {
|
||||
// Name of the exchange
|
||||
Name string
|
||||
// Type of the exchange
|
||||
Type MQExchangeType
|
||||
// Whether its persistent
|
||||
Durable bool
|
||||
}
|
||||
|
||||
func newRabbitMQConn(ex Exchange, urls []string, prefetchCount int, prefetchGlobal bool, confirmPublish bool, withoutExchange bool, logger logger.Logger) *rabbitMQConn {
|
||||
var url string
|
||||
|
||||
if len(urls) > 0 && regexp.MustCompile("^amqp(s)?://.*").MatchString(urls[0]) {
|
||||
url = urls[0]
|
||||
} else {
|
||||
url = DefaultRabbitURL
|
||||
}
|
||||
|
||||
ret := &rabbitMQConn{
|
||||
exchange: ex,
|
||||
url: url,
|
||||
withoutExchange: withoutExchange,
|
||||
prefetchCount: prefetchCount,
|
||||
prefetchGlobal: prefetchGlobal,
|
||||
confirmPublish: confirmPublish,
|
||||
close: make(chan bool),
|
||||
waitConnection: make(chan struct{}),
|
||||
logger: logger,
|
||||
}
|
||||
// its bad case of nil == waitConnection, so close it at start
|
||||
close(ret.waitConnection)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (r *rabbitMQConn) connect(secure bool, config *amqp.Config) error {
|
||||
// try connect
|
||||
if err := r.tryConnect(secure, config); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// connected
|
||||
r.Lock()
|
||||
r.connected = true
|
||||
r.Unlock()
|
||||
|
||||
// create reconnect loop
|
||||
go r.reconnect(secure, config)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rabbitMQConn) reconnect(secure bool, config *amqp.Config) {
|
||||
// skip first connect
|
||||
var connect bool
|
||||
|
||||
for {
|
||||
if connect {
|
||||
// try reconnect
|
||||
if err := r.tryConnect(secure, config); err != nil {
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
// connected
|
||||
r.Lock()
|
||||
r.connected = true
|
||||
r.Unlock()
|
||||
// unblock resubscribe cycle - close channel
|
||||
//at this point channel is created and unclosed - close it without any additional checks
|
||||
close(r.waitConnection)
|
||||
}
|
||||
|
||||
connect = true
|
||||
notifyClose := make(chan *amqp.Error)
|
||||
r.Connection.NotifyClose(notifyClose)
|
||||
chanNotifyClose := make(chan *amqp.Error)
|
||||
var channel *amqp.Channel
|
||||
if !r.withoutExchange {
|
||||
channel = r.ExchangeChannel.channel
|
||||
} else {
|
||||
channel = r.Channel.channel
|
||||
}
|
||||
channel.NotifyClose(chanNotifyClose)
|
||||
// To avoid deadlocks it is necessary to consume the messages from all channels.
|
||||
for notifyClose != nil || chanNotifyClose != nil {
|
||||
// block until closed
|
||||
select {
|
||||
case err := <-chanNotifyClose:
|
||||
r.logger.Log(logger.ErrorLevel, err)
|
||||
// block all resubscribe attempt - they are useless because there is no connection to rabbitmq
|
||||
// create channel 'waitConnection' (at this point channel is nil or closed, create it without unnecessary checks)
|
||||
r.Lock()
|
||||
r.connected = false
|
||||
r.waitConnection = make(chan struct{})
|
||||
r.Unlock()
|
||||
chanNotifyClose = nil
|
||||
case err := <-notifyClose:
|
||||
r.logger.Log(logger.ErrorLevel, err)
|
||||
// block all resubscribe attempt - they are useless because there is no connection to rabbitmq
|
||||
// create channel 'waitConnection' (at this point channel is nil or closed, create it without unnecessary checks)
|
||||
r.Lock()
|
||||
r.connected = false
|
||||
r.waitConnection = make(chan struct{})
|
||||
r.Unlock()
|
||||
notifyClose = nil
|
||||
case <-r.close:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rabbitMQConn) Connect(secure bool, config *amqp.Config) error {
|
||||
r.Lock()
|
||||
|
||||
// already connected
|
||||
if r.connected {
|
||||
r.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// check it was closed
|
||||
select {
|
||||
case <-r.close:
|
||||
r.close = make(chan bool)
|
||||
default:
|
||||
// no op
|
||||
// new conn
|
||||
}
|
||||
|
||||
r.Unlock()
|
||||
|
||||
return r.connect(secure, config)
|
||||
}
|
||||
|
||||
func (r *rabbitMQConn) Close() error {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
select {
|
||||
case <-r.close:
|
||||
return nil
|
||||
default:
|
||||
close(r.close)
|
||||
r.connected = false
|
||||
}
|
||||
|
||||
return r.Connection.Close()
|
||||
}
|
||||
|
||||
func (r *rabbitMQConn) tryConnect(secure bool, config *amqp.Config) error {
|
||||
var err error
|
||||
|
||||
if config == nil {
|
||||
config = &defaultAmqpConfig
|
||||
}
|
||||
|
||||
url := r.url
|
||||
|
||||
if secure || config.TLSClientConfig != nil || strings.HasPrefix(r.url, "amqps://") {
|
||||
if config.TLSClientConfig == nil {
|
||||
// Use environment-based config - secure by default
|
||||
config.TLSClientConfig = mtls.Config()
|
||||
}
|
||||
|
||||
url = strings.Replace(r.url, "amqp://", "amqps://", 1)
|
||||
}
|
||||
|
||||
r.Connection, err = dialConfig(url, *config)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if r.Channel, err = newRabbitChannel(r.Connection, r.prefetchCount, r.prefetchGlobal, r.confirmPublish); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !r.withoutExchange {
|
||||
if r.exchange.Durable {
|
||||
_ = r.Channel.DeclareDurableExchange(r.exchange)
|
||||
} else {
|
||||
_ = r.Channel.DeclareExchange(r.exchange)
|
||||
}
|
||||
r.ExchangeChannel, err = newRabbitChannel(r.Connection, r.prefetchCount, r.prefetchGlobal, r.confirmPublish)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *rabbitMQConn) Consume(queue, key string, headers amqp.Table, qArgs amqp.Table, autoAck, durableQueue bool) (*rabbitMQChannel, <-chan amqp.Delivery, error) {
|
||||
consumerChannel, err := newRabbitChannel(r.Connection, r.prefetchCount, r.prefetchGlobal, r.confirmPublish)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if durableQueue {
|
||||
err = consumerChannel.DeclareDurableQueue(queue, qArgs)
|
||||
} else {
|
||||
err = consumerChannel.DeclareQueue(queue, qArgs)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
deliveries, err := consumerChannel.ConsumeQueue(queue, autoAck)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if !r.withoutExchange {
|
||||
err = consumerChannel.BindQueue(queue, key, r.exchange.Name, headers)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return consumerChannel, deliveries, nil
|
||||
}
|
||||
|
||||
func (r *rabbitMQConn) Publish(exchange, key string, msg amqp.Publishing) error {
|
||||
if r.withoutExchange {
|
||||
return r.Channel.Publish("", key, msg)
|
||||
}
|
||||
return r.ExchangeChannel.Publish(exchange, key, msg)
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package rabbitmq
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
"go-micro.dev/v6/logger"
|
||||
)
|
||||
|
||||
func TestNewRabbitMQConnURL(t *testing.T) {
|
||||
testcases := []struct {
|
||||
title string
|
||||
urls []string
|
||||
want string
|
||||
}{
|
||||
{"Multiple URLs", []string{"amqp://example.com/one", "amqp://example.com/two"}, "amqp://example.com/one"},
|
||||
{"Insecure URL", []string{"amqp://example.com"}, "amqp://example.com"},
|
||||
{"Secure URL", []string{"amqps://example.com"}, "amqps://example.com"},
|
||||
{"Invalid URL", []string{"http://example.com"}, DefaultRabbitURL},
|
||||
{"No URLs", []string{}, DefaultRabbitURL},
|
||||
}
|
||||
|
||||
for _, test := range testcases {
|
||||
conn := newRabbitMQConn(Exchange{Name: "exchange"}, test.urls, 0, false, false, false, logger.DefaultLogger)
|
||||
|
||||
if have, want := conn.url, test.want; have != want {
|
||||
t.Errorf("%s: invalid url, want %q, have %q", test.title, want, have)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTryToConnectTLS(t *testing.T) {
|
||||
var (
|
||||
dialCount, dialTLSCount int
|
||||
|
||||
err = errors.New("stop connect here")
|
||||
)
|
||||
|
||||
dialConfig = func(_ string, c amqp.Config) (*amqp.Connection, error) {
|
||||
if c.TLSClientConfig != nil {
|
||||
dialTLSCount++
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dialCount++
|
||||
return nil, err
|
||||
}
|
||||
|
||||
testcases := []struct {
|
||||
title string
|
||||
url string
|
||||
secure bool
|
||||
amqpConfig *amqp.Config
|
||||
wantTLS bool
|
||||
}{
|
||||
{"unsecure url, secure false, no tls config", "amqp://example.com", false, nil, false},
|
||||
{"secure url, secure false, no tls config", "amqps://example.com", false, nil, true},
|
||||
{"unsecure url, secure true, no tls config", "amqp://example.com", true, nil, true},
|
||||
{"unsecure url, secure false, tls config", "amqp://example.com", false, &amqp.Config{TLSClientConfig: &tls.Config{}}, true},
|
||||
}
|
||||
|
||||
for _, test := range testcases {
|
||||
dialCount, dialTLSCount = 0, 0
|
||||
|
||||
conn := newRabbitMQConn(Exchange{Name: "exchange"}, []string{test.url}, 0, false, false, false, logger.DefaultLogger)
|
||||
conn.tryConnect(test.secure, test.amqpConfig)
|
||||
|
||||
have := dialCount
|
||||
if test.wantTLS {
|
||||
have = dialTLSCount
|
||||
}
|
||||
|
||||
if have != 1 {
|
||||
t.Errorf("%s: used wrong dialer, Dial called %d times, DialTLS called %d times", test.title, dialCount, dialTLSCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRabbitMQPrefetchConfirmPublish(t *testing.T) {
|
||||
testcases := []struct {
|
||||
title string
|
||||
urls []string
|
||||
prefetchCount int
|
||||
prefetchGlobal bool
|
||||
confirmPublish bool
|
||||
}{
|
||||
{"Multiple URLs", []string{"amqp://example.com/one", "amqp://example.com/two"}, 1, true, true},
|
||||
{"Insecure URL", []string{"amqp://example.com"}, 1, true, true},
|
||||
{"Secure URL", []string{"amqps://example.com"}, 1, true, true},
|
||||
{"Invalid URL", []string{"http://example.com"}, 1, true, true},
|
||||
{"No URLs", []string{}, 1, true, true},
|
||||
}
|
||||
|
||||
for _, test := range testcases {
|
||||
conn := newRabbitMQConn(Exchange{Name: "exchange"}, test.urls, test.prefetchCount, test.prefetchGlobal, test.confirmPublish, false, logger.DefaultLogger)
|
||||
|
||||
if have, want := conn.prefetchCount, test.prefetchCount; have != want {
|
||||
t.Errorf("%s: invalid prefetch count, want %d, have %d", test.title, want, have)
|
||||
}
|
||||
|
||||
if have, want := conn.prefetchGlobal, test.prefetchGlobal; have != want {
|
||||
t.Errorf("%s: invalid prefetch global setting, want %t, have %t", test.title, want, have)
|
||||
}
|
||||
|
||||
if have, want := conn.confirmPublish, test.confirmPublish; have != want {
|
||||
t.Errorf("%s: invalid confirm setting, want %t, have %t", test.title, want, have)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package rabbitmq
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go-micro.dev/v6/broker"
|
||||
"go-micro.dev/v6/server"
|
||||
)
|
||||
|
||||
// setSubscribeOption returns a function to setup a context with given value.
|
||||
func setSubscribeOption(k, v interface{}) broker.SubscribeOption {
|
||||
return func(o *broker.SubscribeOptions) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// setBrokerOption returns a function to setup a context with given value.
|
||||
func setBrokerOption(k, v interface{}) broker.Option {
|
||||
return func(o *broker.Options) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// setBrokerOption returns a function to setup a context with given value.
|
||||
func setServerSubscriberOption(k, v interface{}) server.SubscriberOption {
|
||||
return func(o *server.SubscriberOptions) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// setPublishOption returns a function to setup a context with given value.
|
||||
func setPublishOption(k, v interface{}) broker.PublishOption {
|
||||
return func(o *broker.PublishOptions) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, k, v)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package rabbitmq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v6/broker"
|
||||
"go-micro.dev/v6/client"
|
||||
"go-micro.dev/v6/server"
|
||||
)
|
||||
|
||||
type durableQueueKey struct{}
|
||||
type headersKey struct{}
|
||||
type queueArgumentsKey struct{}
|
||||
type prefetchCountKey struct{}
|
||||
type prefetchGlobalKey struct{}
|
||||
type confirmPublishKey struct{}
|
||||
type exchangeKey struct{}
|
||||
type exchangeTypeKey struct{}
|
||||
type withoutExchangeKey struct{}
|
||||
type requeueOnErrorKey struct{}
|
||||
type deliveryMode struct{}
|
||||
type priorityKey struct{}
|
||||
type contentType struct{}
|
||||
type contentEncoding struct{}
|
||||
type correlationID struct{}
|
||||
type replyTo struct{}
|
||||
type expiration struct{}
|
||||
type messageID struct{}
|
||||
type timestamp struct{}
|
||||
type typeMsg struct{}
|
||||
type userID struct{}
|
||||
type appID struct{}
|
||||
type externalAuth struct{}
|
||||
type durableExchange struct{}
|
||||
|
||||
// ServerDurableQueue provide durable queue option for micro.RegisterSubscriber
|
||||
func ServerDurableQueue() server.SubscriberOption {
|
||||
return setServerSubscriberOption(durableQueueKey{}, true)
|
||||
}
|
||||
|
||||
// ServerAckOnSuccess export AckOnSuccess server.SubscriberOption
|
||||
func ServerAckOnSuccess() server.SubscriberOption {
|
||||
return setServerSubscriberOption(ackSuccessKey{}, true)
|
||||
}
|
||||
|
||||
// DurableQueue creates a durable queue when subscribing.
|
||||
func DurableQueue() broker.SubscribeOption {
|
||||
return setSubscribeOption(durableQueueKey{}, true)
|
||||
}
|
||||
|
||||
// DurableExchange is an option to set the Exchange to be durable.
|
||||
func DurableExchange() broker.Option {
|
||||
return setBrokerOption(durableExchange{}, true)
|
||||
}
|
||||
|
||||
// Headers adds headers used by the headers exchange.
|
||||
func Headers(h map[string]interface{}) broker.SubscribeOption {
|
||||
return setSubscribeOption(headersKey{}, h)
|
||||
}
|
||||
|
||||
// QueueArguments sets arguments for queue creation.
|
||||
func QueueArguments(h map[string]interface{}) broker.SubscribeOption {
|
||||
return setSubscribeOption(queueArgumentsKey{}, h)
|
||||
}
|
||||
|
||||
func RequeueOnError() broker.SubscribeOption {
|
||||
return setSubscribeOption(requeueOnErrorKey{}, true)
|
||||
}
|
||||
|
||||
// ExchangeName is an option to set the ExchangeName.
|
||||
func ExchangeName(e string) broker.Option {
|
||||
return setBrokerOption(exchangeKey{}, e)
|
||||
}
|
||||
|
||||
// WithoutExchange is an option to use the rabbitmq default exchange.
|
||||
// means it would not create any custom exchange.
|
||||
func WithoutExchange() broker.Option {
|
||||
return setBrokerOption(withoutExchangeKey{}, true)
|
||||
}
|
||||
|
||||
// ExchangeType is an option to set the rabbitmq exchange type.
|
||||
func ExchangeType(t MQExchangeType) broker.Option {
|
||||
return setBrokerOption(exchangeTypeKey{}, t)
|
||||
}
|
||||
|
||||
// PrefetchCount ...
|
||||
func PrefetchCount(c int) broker.Option {
|
||||
return setBrokerOption(prefetchCountKey{}, c)
|
||||
}
|
||||
|
||||
// PrefetchGlobal creates a durable queue when subscribing.
|
||||
func PrefetchGlobal() broker.Option {
|
||||
return setBrokerOption(prefetchGlobalKey{}, true)
|
||||
}
|
||||
|
||||
// ConfirmPublish ensures all published messages are confirmed by waiting for an ack/nack from the broker.
|
||||
func ConfirmPublish() broker.Option {
|
||||
return setBrokerOption(confirmPublishKey{}, true)
|
||||
}
|
||||
|
||||
// DeliveryMode sets a delivery mode for publishing.
|
||||
func DeliveryMode(value uint8) broker.PublishOption {
|
||||
return setPublishOption(deliveryMode{}, value)
|
||||
}
|
||||
|
||||
// Priority sets a priority level for publishing.
|
||||
func Priority(value uint8) broker.PublishOption {
|
||||
return setPublishOption(priorityKey{}, value)
|
||||
}
|
||||
|
||||
// ContentType sets a property MIME content type for publishing.
|
||||
func ContentType(value string) broker.PublishOption {
|
||||
return setPublishOption(contentType{}, value)
|
||||
}
|
||||
|
||||
// ContentEncoding sets a property MIME content encoding for publishing.
|
||||
func ContentEncoding(value string) broker.PublishOption {
|
||||
return setPublishOption(contentEncoding{}, value)
|
||||
}
|
||||
|
||||
// CorrelationID sets a property correlation ID for publishing.
|
||||
func CorrelationID(value string) broker.PublishOption {
|
||||
return setPublishOption(correlationID{}, value)
|
||||
}
|
||||
|
||||
// ReplyTo sets a property address to to reply to (ex: RPC) for publishing.
|
||||
func ReplyTo(value string) broker.PublishOption {
|
||||
return setPublishOption(replyTo{}, value)
|
||||
}
|
||||
|
||||
// Expiration sets a property message expiration spec for publishing.
|
||||
func Expiration(value string) broker.PublishOption {
|
||||
return setPublishOption(expiration{}, value)
|
||||
}
|
||||
|
||||
// MessageId sets a property message identifier for publishing.
|
||||
func MessageId(value string) broker.PublishOption {
|
||||
return setPublishOption(messageID{}, value)
|
||||
}
|
||||
|
||||
// Timestamp sets a property message timestamp for publishing.
|
||||
func Timestamp(value time.Time) broker.PublishOption {
|
||||
return setPublishOption(timestamp{}, value)
|
||||
}
|
||||
|
||||
// TypeMsg sets a property message type name for publishing.
|
||||
func TypeMsg(value string) broker.PublishOption {
|
||||
return setPublishOption(typeMsg{}, value)
|
||||
}
|
||||
|
||||
// UserID sets a property user id for publishing.
|
||||
func UserID(value string) broker.PublishOption {
|
||||
return setPublishOption(userID{}, value)
|
||||
}
|
||||
|
||||
// AppID sets a property application id for publishing.
|
||||
func AppID(value string) broker.PublishOption {
|
||||
return setPublishOption(appID{}, value)
|
||||
}
|
||||
|
||||
func ExternalAuth() broker.Option {
|
||||
return setBrokerOption(externalAuth{}, ExternalAuthentication{})
|
||||
}
|
||||
|
||||
type subscribeContextKey struct{}
|
||||
|
||||
// SubscribeContext set the context for broker.SubscribeOption.
|
||||
func SubscribeContext(ctx context.Context) broker.SubscribeOption {
|
||||
return setSubscribeOption(subscribeContextKey{}, ctx)
|
||||
}
|
||||
|
||||
type ackSuccessKey struct{}
|
||||
|
||||
// AckOnSuccess will automatically acknowledge messages when no error is returned.
|
||||
func AckOnSuccess() broker.SubscribeOption {
|
||||
return setSubscribeOption(ackSuccessKey{}, true)
|
||||
}
|
||||
|
||||
// PublishDeliveryMode client.PublishOption for setting message "delivery mode"
|
||||
// mode , Transient (0 or 1) or Persistent (2)
|
||||
func PublishDeliveryMode(mode uint8) client.PublishOption {
|
||||
return func(o *client.PublishOptions) {
|
||||
if o.Context == nil {
|
||||
o.Context = context.Background()
|
||||
}
|
||||
o.Context = context.WithValue(o.Context, deliveryMode{}, mode)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
// Package rabbitmq provides a RabbitMQ broker
|
||||
package rabbitmq
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
"go-micro.dev/v6/broker"
|
||||
"go-micro.dev/v6/logger"
|
||||
)
|
||||
|
||||
type rbroker struct {
|
||||
conn *rabbitMQConn
|
||||
addrs []string
|
||||
opts broker.Options
|
||||
mtx sync.Mutex
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
type subscriber struct {
|
||||
mtx sync.Mutex
|
||||
unsub chan bool
|
||||
opts broker.SubscribeOptions
|
||||
topic string
|
||||
ch *rabbitMQChannel
|
||||
durableQueue bool
|
||||
queueArgs map[string]interface{}
|
||||
r *rbroker
|
||||
fn func(msg amqp.Delivery)
|
||||
headers map[string]interface{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
type publication struct {
|
||||
d amqp.Delivery
|
||||
m *broker.Message
|
||||
t string
|
||||
err error
|
||||
}
|
||||
|
||||
func (p *publication) Ack() error {
|
||||
return p.d.Ack(false)
|
||||
}
|
||||
|
||||
func (p *publication) Error() error {
|
||||
return p.err
|
||||
}
|
||||
|
||||
func (p *publication) Topic() string {
|
||||
return p.t
|
||||
}
|
||||
|
||||
func (p *publication) Message() *broker.Message {
|
||||
return p.m
|
||||
}
|
||||
|
||||
func (s *subscriber) Options() broker.SubscribeOptions {
|
||||
return s.opts
|
||||
}
|
||||
|
||||
func (s *subscriber) Topic() string {
|
||||
return s.topic
|
||||
}
|
||||
|
||||
func (s *subscriber) Unsubscribe() error {
|
||||
s.unsub <- true
|
||||
|
||||
// Need to wait on subscriber to exit if autoack is disabled
|
||||
// since closing the channel will prevent the ack/nack from
|
||||
// being sent upon handler completion.
|
||||
if !s.opts.AutoAck {
|
||||
s.wg.Wait()
|
||||
}
|
||||
|
||||
s.mtx.Lock()
|
||||
defer s.mtx.Unlock()
|
||||
if s.ch != nil {
|
||||
return s.ch.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *subscriber) resubscribe() {
|
||||
s.wg.Add(1)
|
||||
defer s.wg.Done()
|
||||
|
||||
minResubscribeDelay := 100 * time.Millisecond
|
||||
maxResubscribeDelay := 30 * time.Second
|
||||
expFactor := time.Duration(2)
|
||||
reSubscribeDelay := minResubscribeDelay
|
||||
// loop until unsubscribe
|
||||
for {
|
||||
select {
|
||||
// unsubscribe case
|
||||
case <-s.unsub:
|
||||
return
|
||||
// check shutdown case
|
||||
case <-s.r.conn.close:
|
||||
// yep, its shutdown case
|
||||
return
|
||||
// wait until we reconect to rabbit
|
||||
case <-s.r.conn.waitConnection:
|
||||
// When the connection is disconnected, the waitConnection will be re-assigned, so '<-s.r.conn.waitConnection' maybe blocked.
|
||||
// Here, it returns once a second, and then the latest waitconnection will be used
|
||||
case <-time.After(time.Second):
|
||||
continue
|
||||
}
|
||||
|
||||
// it may crash (panic) in case of Consume without connection, so recheck it
|
||||
s.r.mtx.Lock()
|
||||
if !s.r.conn.connected {
|
||||
s.r.mtx.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
ch, sub, err := s.r.conn.Consume(
|
||||
s.opts.Queue,
|
||||
s.topic,
|
||||
s.headers,
|
||||
s.queueArgs,
|
||||
s.opts.AutoAck,
|
||||
s.durableQueue,
|
||||
)
|
||||
|
||||
s.r.mtx.Unlock()
|
||||
switch err {
|
||||
case nil:
|
||||
reSubscribeDelay = minResubscribeDelay
|
||||
s.mtx.Lock()
|
||||
s.ch = ch
|
||||
s.mtx.Unlock()
|
||||
default:
|
||||
if reSubscribeDelay > maxResubscribeDelay {
|
||||
reSubscribeDelay = maxResubscribeDelay
|
||||
}
|
||||
time.Sleep(reSubscribeDelay)
|
||||
reSubscribeDelay *= expFactor
|
||||
continue
|
||||
}
|
||||
|
||||
SubLoop:
|
||||
for {
|
||||
select {
|
||||
case <-s.unsub:
|
||||
return
|
||||
case d, ok := <-sub:
|
||||
if !ok {
|
||||
break SubLoop
|
||||
}
|
||||
s.r.wg.Add(1)
|
||||
s.fn(d)
|
||||
s.r.wg.Done()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rbroker) Publish(topic string, msg *broker.Message, opts ...broker.PublishOption) error {
|
||||
m := amqp.Publishing{
|
||||
Body: msg.Body,
|
||||
Headers: amqp.Table{},
|
||||
}
|
||||
|
||||
options := broker.PublishOptions{}
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
if options.Context != nil {
|
||||
if value, ok := options.Context.Value(deliveryMode{}).(uint8); ok {
|
||||
m.DeliveryMode = value
|
||||
}
|
||||
|
||||
if value, ok := options.Context.Value(priorityKey{}).(uint8); ok {
|
||||
m.Priority = value
|
||||
}
|
||||
|
||||
if value, ok := options.Context.Value(contentType{}).(string); ok {
|
||||
m.Headers["Content-Type"] = value
|
||||
m.ContentType = value
|
||||
}
|
||||
|
||||
if value, ok := options.Context.Value(contentEncoding{}).(string); ok {
|
||||
m.ContentEncoding = value
|
||||
}
|
||||
|
||||
if value, ok := options.Context.Value(correlationID{}).(string); ok {
|
||||
m.CorrelationId = value
|
||||
}
|
||||
|
||||
if value, ok := options.Context.Value(replyTo{}).(string); ok {
|
||||
m.ReplyTo = value
|
||||
}
|
||||
|
||||
if value, ok := options.Context.Value(expiration{}).(string); ok {
|
||||
m.Expiration = value
|
||||
}
|
||||
|
||||
if value, ok := options.Context.Value(messageID{}).(string); ok {
|
||||
m.MessageId = value
|
||||
}
|
||||
|
||||
if value, ok := options.Context.Value(timestamp{}).(time.Time); ok {
|
||||
m.Timestamp = value
|
||||
}
|
||||
|
||||
if value, ok := options.Context.Value(typeMsg{}).(string); ok {
|
||||
m.Type = value
|
||||
}
|
||||
|
||||
if value, ok := options.Context.Value(userID{}).(string); ok {
|
||||
m.UserId = value
|
||||
}
|
||||
|
||||
if value, ok := options.Context.Value(appID{}).(string); ok {
|
||||
m.AppId = value
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range msg.Header {
|
||||
m.Headers[k] = v
|
||||
}
|
||||
|
||||
if r.getWithoutExchange() {
|
||||
m.Headers["Micro-Topic"] = topic
|
||||
}
|
||||
|
||||
if r.conn == nil {
|
||||
return errors.New("connection is nil")
|
||||
}
|
||||
|
||||
return r.conn.Publish(r.conn.exchange.Name, topic, m)
|
||||
}
|
||||
|
||||
func (r *rbroker) Subscribe(topic string, handler broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) {
|
||||
var ackSuccess bool
|
||||
|
||||
if r.conn == nil {
|
||||
return nil, errors.New("not connected")
|
||||
}
|
||||
|
||||
opt := broker.SubscribeOptions{
|
||||
AutoAck: true,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&opt)
|
||||
}
|
||||
|
||||
// Make sure context is setup
|
||||
if opt.Context == nil {
|
||||
opt.Context = context.Background()
|
||||
}
|
||||
|
||||
ctx := opt.Context
|
||||
if subscribeContext, ok := ctx.Value(subscribeContextKey{}).(context.Context); ok && subscribeContext != nil {
|
||||
ctx = subscribeContext
|
||||
}
|
||||
|
||||
var requeueOnError bool
|
||||
requeueOnError, _ = ctx.Value(requeueOnErrorKey{}).(bool)
|
||||
|
||||
var durableQueue bool
|
||||
durableQueue, _ = ctx.Value(durableQueueKey{}).(bool)
|
||||
|
||||
var qArgs map[string]interface{}
|
||||
if qa, ok := ctx.Value(queueArgumentsKey{}).(map[string]interface{}); ok {
|
||||
qArgs = qa
|
||||
}
|
||||
|
||||
var headers map[string]interface{}
|
||||
if h, ok := ctx.Value(headersKey{}).(map[string]interface{}); ok {
|
||||
headers = h
|
||||
}
|
||||
|
||||
if bval, ok := ctx.Value(ackSuccessKey{}).(bool); ok && bval {
|
||||
opt.AutoAck = false
|
||||
ackSuccess = true
|
||||
}
|
||||
|
||||
fn := func(msg amqp.Delivery) {
|
||||
header := make(map[string]string)
|
||||
for k, v := range msg.Headers {
|
||||
header[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
|
||||
// Get rid of dependence on 'Micro-Topic'
|
||||
msgTopic := header["Micro-Topic"]
|
||||
if msgTopic == "" {
|
||||
header["Micro-Topic"] = msg.RoutingKey
|
||||
}
|
||||
|
||||
m := &broker.Message{
|
||||
Header: header,
|
||||
Body: msg.Body,
|
||||
}
|
||||
p := &publication{d: msg, m: m, t: msg.RoutingKey}
|
||||
p.err = handler(p)
|
||||
if p.err == nil && ackSuccess && !opt.AutoAck {
|
||||
_ = msg.Ack(false)
|
||||
} else if p.err != nil && !opt.AutoAck {
|
||||
_ = msg.Nack(false, requeueOnError)
|
||||
}
|
||||
}
|
||||
|
||||
sret := &subscriber{topic: topic, opts: opt, unsub: make(chan bool), r: r,
|
||||
durableQueue: durableQueue, fn: fn, headers: headers, queueArgs: qArgs,
|
||||
wg: sync.WaitGroup{}}
|
||||
|
||||
go sret.resubscribe()
|
||||
|
||||
return sret, nil
|
||||
}
|
||||
|
||||
func (r *rbroker) Options() broker.Options {
|
||||
return r.opts
|
||||
}
|
||||
|
||||
func (r *rbroker) String() string {
|
||||
return "rabbitmq"
|
||||
}
|
||||
|
||||
func (r *rbroker) Address() string {
|
||||
if len(r.addrs) > 0 {
|
||||
u, err := url.Parse(r.addrs[0])
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return u.Redacted()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (r *rbroker) Init(opts ...broker.Option) error {
|
||||
for _, o := range opts {
|
||||
o(&r.opts)
|
||||
}
|
||||
r.addrs = r.opts.Addrs
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *rbroker) Connect() error {
|
||||
if r.conn == nil {
|
||||
r.conn = newRabbitMQConn(
|
||||
r.getExchange(),
|
||||
r.opts.Addrs,
|
||||
r.getPrefetchCount(),
|
||||
r.getPrefetchGlobal(),
|
||||
r.getConfirmPublish(),
|
||||
r.getWithoutExchange(),
|
||||
r.opts.Logger,
|
||||
)
|
||||
}
|
||||
|
||||
conf := defaultAmqpConfig
|
||||
|
||||
if auth, ok := r.opts.Context.Value(externalAuth{}).(ExternalAuthentication); ok {
|
||||
conf.SASL = []amqp.Authentication{&auth}
|
||||
}
|
||||
|
||||
conf.TLSClientConfig = r.opts.TLSConfig
|
||||
|
||||
return r.conn.Connect(r.opts.Secure, &conf)
|
||||
}
|
||||
|
||||
func (r *rbroker) Disconnect() error {
|
||||
if r.conn == nil {
|
||||
return errors.New("connection is nil")
|
||||
}
|
||||
ret := r.conn.Close()
|
||||
r.wg.Wait() // wait all goroutines
|
||||
return ret
|
||||
}
|
||||
|
||||
func NewBroker(opts ...broker.Option) broker.Broker {
|
||||
options := broker.Options{
|
||||
Context: context.Background(),
|
||||
Logger: logger.DefaultLogger,
|
||||
}
|
||||
|
||||
for _, o := range opts {
|
||||
o(&options)
|
||||
}
|
||||
|
||||
return &rbroker{
|
||||
addrs: options.Addrs,
|
||||
opts: options,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *rbroker) getExchange() Exchange {
|
||||
ex := DefaultExchange
|
||||
|
||||
if e, ok := r.opts.Context.Value(exchangeKey{}).(string); ok {
|
||||
ex.Name = e
|
||||
}
|
||||
|
||||
if t, ok := r.opts.Context.Value(exchangeTypeKey{}).(MQExchangeType); ok {
|
||||
ex.Type = t
|
||||
}
|
||||
|
||||
if d, ok := r.opts.Context.Value(durableExchange{}).(bool); ok {
|
||||
ex.Durable = d
|
||||
}
|
||||
|
||||
return ex
|
||||
}
|
||||
|
||||
func (r *rbroker) getPrefetchCount() int {
|
||||
if e, ok := r.opts.Context.Value(prefetchCountKey{}).(int); ok {
|
||||
return e
|
||||
}
|
||||
return DefaultPrefetchCount
|
||||
}
|
||||
|
||||
func (r *rbroker) getPrefetchGlobal() bool {
|
||||
if e, ok := r.opts.Context.Value(prefetchGlobalKey{}).(bool); ok {
|
||||
return e
|
||||
}
|
||||
return DefaultPrefetchGlobal
|
||||
}
|
||||
|
||||
func (r *rbroker) getConfirmPublish() bool {
|
||||
if e, ok := r.opts.Context.Value(confirmPublishKey{}).(bool); ok {
|
||||
return e
|
||||
}
|
||||
return DefaultConfirmPublish
|
||||
}
|
||||
|
||||
func (r *rbroker) getWithoutExchange() bool {
|
||||
if e, ok := r.opts.Context.Value(withoutExchangeKey{}).(bool); ok {
|
||||
return e
|
||||
}
|
||||
return DefaultWithoutExchange
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
package rabbitmq_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v6/logger"
|
||||
|
||||
micro "go-micro.dev/v6"
|
||||
broker "go-micro.dev/v6/broker"
|
||||
rabbitmq "go-micro.dev/v6/broker/rabbitmq"
|
||||
server "go-micro.dev/v6/server"
|
||||
)
|
||||
|
||||
type Example struct{}
|
||||
|
||||
func init() {
|
||||
rabbitmq.DefaultRabbitURL = "amqp://rabbitmq:rabbitmq@127.0.0.1:5672"
|
||||
}
|
||||
|
||||
type TestEvent struct {
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
Time time.Time `json:"time"`
|
||||
}
|
||||
|
||||
func (e *Example) Handler(ctx context.Context, r interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestDurable(t *testing.T) {
|
||||
if tr := os.Getenv("TRAVIS"); len(tr) > 0 {
|
||||
t.Skip()
|
||||
}
|
||||
brkrSub := broker.NewSubscribeOptions(
|
||||
broker.Queue("queue.default"),
|
||||
broker.DisableAutoAck(),
|
||||
rabbitmq.DurableQueue(),
|
||||
)
|
||||
|
||||
b := rabbitmq.NewBroker()
|
||||
b.Init()
|
||||
if err := b.Connect(); err != nil {
|
||||
t.Logf("cant conect to broker, skip: %v", err)
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
s := server.NewServer(server.Broker(b))
|
||||
|
||||
service := micro.NewService("test", micro.Server(s),
|
||||
micro.Broker(b),
|
||||
)
|
||||
h := &Example{}
|
||||
// Register a subscriber
|
||||
micro.RegisterSubscriber(
|
||||
"topic",
|
||||
service.Server(),
|
||||
h.Handler,
|
||||
server.SubscriberContext(brkrSub.Context),
|
||||
server.SubscriberQueue("queue.default"),
|
||||
)
|
||||
|
||||
// service.Init()
|
||||
|
||||
if err := service.Run(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithoutExchange(t *testing.T) {
|
||||
|
||||
b := rabbitmq.NewBroker(rabbitmq.WithoutExchange())
|
||||
b.Init()
|
||||
if err := b.Connect(); err != nil {
|
||||
t.Logf("cant conect to broker, skip: %v", err)
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
s := server.NewServer(server.Broker(b))
|
||||
|
||||
service := micro.NewService("test", micro.Server(s),
|
||||
micro.Broker(b),
|
||||
)
|
||||
brkrSub := broker.NewSubscribeOptions(
|
||||
broker.Queue("direct.queue"),
|
||||
broker.DisableAutoAck(),
|
||||
rabbitmq.DurableQueue(),
|
||||
)
|
||||
// Register a subscriber
|
||||
err := micro.RegisterSubscriber(
|
||||
"direct.queue",
|
||||
service.Server(),
|
||||
func(ctx context.Context, evt *TestEvent) error {
|
||||
logger.Logf(logger.InfoLevel, "receive event: %+v", evt)
|
||||
return nil
|
||||
},
|
||||
server.SubscriberContext(brkrSub.Context),
|
||||
server.SubscriberQueue("direct.queue"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
time.Sleep(5 * time.Second)
|
||||
logger.Logf(logger.InfoLevel, "pub event")
|
||||
jsonData, _ := json.Marshal(&TestEvent{
|
||||
Name: "test",
|
||||
Age: 16,
|
||||
})
|
||||
err := b.Publish("direct.queue", &broker.Message{
|
||||
Body: jsonData,
|
||||
},
|
||||
rabbitmq.DeliveryMode(2),
|
||||
rabbitmq.ContentType("application/json"))
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// service.Init()
|
||||
|
||||
if err := service.Run(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFanoutExchange(t *testing.T) {
|
||||
b := rabbitmq.NewBroker(rabbitmq.ExchangeType(rabbitmq.ExchangeTypeFanout), rabbitmq.ExchangeName("fanout.test"))
|
||||
b.Init()
|
||||
if err := b.Connect(); err != nil {
|
||||
t.Logf("cant conect to broker, skip: %v", err)
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
s := server.NewServer(server.Broker(b))
|
||||
|
||||
service := micro.NewService("test", micro.Server(s),
|
||||
micro.Broker(b),
|
||||
)
|
||||
brkrSub := broker.NewSubscribeOptions(
|
||||
broker.Queue("fanout.queue"),
|
||||
broker.DisableAutoAck(),
|
||||
rabbitmq.DurableQueue(),
|
||||
)
|
||||
// Register a subscriber
|
||||
err := micro.RegisterSubscriber(
|
||||
"fanout.queue",
|
||||
service.Server(),
|
||||
func(ctx context.Context, evt *TestEvent) error {
|
||||
logger.Logf(logger.InfoLevel, "receive event: %+v", evt)
|
||||
return nil
|
||||
},
|
||||
server.SubscriberContext(brkrSub.Context),
|
||||
server.SubscriberQueue("fanout.queue"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
time.Sleep(5 * time.Second)
|
||||
logger.Logf(logger.InfoLevel, "pub event")
|
||||
jsonData, _ := json.Marshal(&TestEvent{
|
||||
Name: "test",
|
||||
Age: 16,
|
||||
})
|
||||
err := b.Publish("fanout.queue", &broker.Message{
|
||||
Body: jsonData,
|
||||
},
|
||||
rabbitmq.DeliveryMode(2),
|
||||
rabbitmq.ContentType("application/json"))
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// service.Init()
|
||||
|
||||
if err := service.Run(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectExchange(t *testing.T) {
|
||||
b := rabbitmq.NewBroker(rabbitmq.ExchangeType(rabbitmq.ExchangeTypeDirect), rabbitmq.ExchangeName("direct.test"))
|
||||
b.Init()
|
||||
if err := b.Connect(); err != nil {
|
||||
t.Logf("cant conect to broker, skip: %v", err)
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
s := server.NewServer(server.Broker(b))
|
||||
|
||||
service := micro.NewService("test", micro.Server(s),
|
||||
micro.Broker(b),
|
||||
)
|
||||
brkrSub := broker.NewSubscribeOptions(
|
||||
broker.Queue("direct.exchange.queue"),
|
||||
broker.DisableAutoAck(),
|
||||
rabbitmq.DurableQueue(),
|
||||
)
|
||||
// Register a subscriber
|
||||
err := micro.RegisterSubscriber(
|
||||
"direct.exchange.queue",
|
||||
service.Server(),
|
||||
func(ctx context.Context, evt *TestEvent) error {
|
||||
logger.Logf(logger.InfoLevel, "receive event: %+v", evt)
|
||||
return nil
|
||||
},
|
||||
server.SubscriberContext(brkrSub.Context),
|
||||
server.SubscriberQueue("direct.exchange.queue"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
time.Sleep(5 * time.Second)
|
||||
logger.Logf(logger.InfoLevel, "pub event")
|
||||
jsonData, _ := json.Marshal(&TestEvent{
|
||||
Name: "test",
|
||||
Age: 16,
|
||||
})
|
||||
err := b.Publish("direct.exchange.queue", &broker.Message{
|
||||
Body: jsonData,
|
||||
},
|
||||
rabbitmq.DeliveryMode(2),
|
||||
rabbitmq.ContentType("application/json"))
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// service.Init()
|
||||
|
||||
if err := service.Run(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTopicExchange(t *testing.T) {
|
||||
b := rabbitmq.NewBroker()
|
||||
b.Init()
|
||||
if err := b.Connect(); err != nil {
|
||||
t.Logf("cant conect to broker, skip: %v", err)
|
||||
t.Skip()
|
||||
}
|
||||
|
||||
s := server.NewServer(server.Broker(b))
|
||||
|
||||
service := micro.NewService("test", micro.Server(s),
|
||||
micro.Broker(b),
|
||||
)
|
||||
brkrSub := broker.NewSubscribeOptions(
|
||||
broker.Queue("topic.exchange.queue"),
|
||||
broker.DisableAutoAck(),
|
||||
rabbitmq.DurableQueue(),
|
||||
)
|
||||
// Register a subscriber
|
||||
err := micro.RegisterSubscriber(
|
||||
"my-test-topic",
|
||||
service.Server(),
|
||||
func(ctx context.Context, evt *TestEvent) error {
|
||||
logger.Logf(logger.InfoLevel, "receive event: %+v", evt)
|
||||
return nil
|
||||
},
|
||||
server.SubscriberContext(brkrSub.Context),
|
||||
server.SubscriberQueue("topic.exchange.queue"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
time.Sleep(5 * time.Second)
|
||||
logger.Logf(logger.InfoLevel, "pub event")
|
||||
jsonData, _ := json.Marshal(&TestEvent{
|
||||
Name: "test",
|
||||
Age: 16,
|
||||
})
|
||||
err := b.Publish("my-test-topic", &broker.Message{
|
||||
Body: jsonData,
|
||||
},
|
||||
rabbitmq.DeliveryMode(2),
|
||||
rabbitmq.ContentType("application/json"))
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// service.Init()
|
||||
|
||||
if err := service.Run(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user