1
0
mirror of https://github.com/go-micro/go-micro.git synced 2025-01-29 18:04:17 +02:00
2021-10-12 12:55:53 +01:00

281 lines
6.3 KiB
Go

package rabbitmq
//
// All credit to Mondo
//
import (
"crypto/tls"
"regexp"
"strings"
"sync"
"time"
"go-micro.dev/v4/logger"
"github.com/streadway/amqp"
)
var (
DefaultExchange = Exchange{
Name: "micro",
}
DefaultRabbitURL = "amqp://guest:guest@127.0.0.1:5672"
DefaultPrefetchCount = 0
DefaultPrefetchGlobal = false
DefaultRequeueOnError = 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,
}
dial = amqp.Dial
dialTLS = amqp.DialTLS
dialConfig = amqp.DialConfig
)
type rabbitMQConn struct {
Connection *amqp.Connection
Channel *rabbitMQChannel
ExchangeChannel *rabbitMQChannel
exchange Exchange
url string
prefetchCount int
prefetchGlobal bool
sync.Mutex
connected bool
close chan bool
waitConnection chan struct{}
}
// Exchange is the rabbitmq exchange
type Exchange struct {
// Name of the exchange
Name string
// Whether its persistent
Durable bool
}
func newRabbitMQConn(ex Exchange, urls []string, prefetchCount int, prefetchGlobal bool) *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,
prefetchCount: prefetchCount,
prefetchGlobal: prefetchGlobal,
close: make(chan bool),
waitConnection: make(chan struct{}),
}
// 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)
channel := r.ExchangeChannel.channel
channel.NotifyClose(chanNotifyClose)
channelNotifyReturn := make(chan amqp.Return)
channel.NotifyReturn(channelNotifyReturn)
// block until closed
select {
case result, ok := <-channelNotifyReturn:
if !ok {
// Channel closed, probably also the channel or connection.
return
}
// Do what you need with messageFailing.
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Errorf("notify error reason: %s, description: %s", result.ReplyText, result.Exchange)
}
case err := <-chanNotifyClose:
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Error(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()
case err := <-notifyClose:
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Error(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()
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 {
config.TLSClientConfig = &tls.Config{
InsecureSkipVerify: true,
}
}
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); err != nil {
return err
}
if r.exchange.Durable {
r.Channel.DeclareDurableExchange(r.exchange.Name)
} else {
r.Channel.DeclareExchange(r.exchange.Name)
}
r.ExchangeChannel, err = newRabbitChannel(r.Connection, r.prefetchCount, r.prefetchGlobal)
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)
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
}
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 {
return r.ExchangeChannel.Publish(exchange, key, msg)
}