go-micro.dev/v5@v5.12.0/broker/rabbitmq/connection.go (about)

     1  package rabbitmq
     2  
     3  //
     4  // All credit to Mondo
     5  //
     6  
     7  import (
     8  	"crypto/tls"
     9  	"regexp"
    10  	"strings"
    11  	"sync"
    12  	"time"
    13  
    14  	amqp "github.com/rabbitmq/amqp091-go"
    15  	"go-micro.dev/v5/logger"
    16  )
    17  
    18  type MQExchangeType string
    19  
    20  const (
    21  	ExchangeTypeFanout MQExchangeType = "fanout"
    22  	ExchangeTypeTopic                 = "topic"
    23  	ExchangeTypeDirect                = "direct"
    24  )
    25  
    26  var (
    27  	DefaultExchange = Exchange{
    28  		Name: "micro",
    29  		Type: ExchangeTypeTopic,
    30  	}
    31  	DefaultRabbitURL       = "amqp://guest:guest@127.0.0.1:5672"
    32  	DefaultPrefetchCount   = 0
    33  	DefaultPrefetchGlobal  = false
    34  	DefaultRequeueOnError  = false
    35  	DefaultConfirmPublish  = false
    36  	DefaultWithoutExchange = false
    37  
    38  	// The amqp library does not seem to set these when using amqp.DialConfig
    39  	// (even though it says so in the comments) so we set them manually to make
    40  	// sure to not brake any existing functionality.
    41  	defaultHeartbeat = 10 * time.Second
    42  	defaultLocale    = "en_US"
    43  
    44  	defaultAmqpConfig = amqp.Config{
    45  		Heartbeat: defaultHeartbeat,
    46  		Locale:    defaultLocale,
    47  	}
    48  
    49  	dial       = amqp.Dial
    50  	dialTLS    = amqp.DialTLS
    51  	dialConfig = amqp.DialConfig
    52  )
    53  
    54  type rabbitMQConn struct {
    55  	Connection      *amqp.Connection
    56  	Channel         *rabbitMQChannel
    57  	ExchangeChannel *rabbitMQChannel
    58  	exchange        Exchange
    59  	withoutExchange bool
    60  	url             string
    61  	prefetchCount   int
    62  	prefetchGlobal  bool
    63  	confirmPublish  bool
    64  
    65  	sync.Mutex
    66  	connected bool
    67  	close     chan bool
    68  
    69  	waitConnection chan struct{}
    70  
    71  	logger logger.Logger
    72  }
    73  
    74  // Exchange is the rabbitmq exchange.
    75  type Exchange struct {
    76  	// Name of the exchange
    77  	Name string
    78  	// Type of the exchange
    79  	Type MQExchangeType
    80  	// Whether its persistent
    81  	Durable bool
    82  }
    83  
    84  func newRabbitMQConn(ex Exchange, urls []string, prefetchCount int, prefetchGlobal bool, confirmPublish bool, withoutExchange bool, logger logger.Logger) *rabbitMQConn {
    85  	var url string
    86  
    87  	if len(urls) > 0 && regexp.MustCompile("^amqp(s)?://.*").MatchString(urls[0]) {
    88  		url = urls[0]
    89  	} else {
    90  		url = DefaultRabbitURL
    91  	}
    92  
    93  	ret := &rabbitMQConn{
    94  		exchange:        ex,
    95  		url:             url,
    96  		withoutExchange: withoutExchange,
    97  		prefetchCount:   prefetchCount,
    98  		prefetchGlobal:  prefetchGlobal,
    99  		confirmPublish:  confirmPublish,
   100  		close:           make(chan bool),
   101  		waitConnection:  make(chan struct{}),
   102  		logger:          logger,
   103  	}
   104  	// its bad case of nil == waitConnection, so close it at start
   105  	close(ret.waitConnection)
   106  	return ret
   107  }
   108  
   109  func (r *rabbitMQConn) connect(secure bool, config *amqp.Config) error {
   110  	// try connect
   111  	if err := r.tryConnect(secure, config); err != nil {
   112  		return err
   113  	}
   114  
   115  	// connected
   116  	r.Lock()
   117  	r.connected = true
   118  	r.Unlock()
   119  
   120  	// create reconnect loop
   121  	go r.reconnect(secure, config)
   122  	return nil
   123  }
   124  
   125  func (r *rabbitMQConn) reconnect(secure bool, config *amqp.Config) {
   126  	// skip first connect
   127  	var connect bool
   128  
   129  	for {
   130  		if connect {
   131  			// try reconnect
   132  			if err := r.tryConnect(secure, config); err != nil {
   133  				time.Sleep(1 * time.Second)
   134  				continue
   135  			}
   136  
   137  			// connected
   138  			r.Lock()
   139  			r.connected = true
   140  			r.Unlock()
   141  			// unblock resubscribe cycle - close channel
   142  			//at this point channel is created and unclosed - close it without any additional checks
   143  			close(r.waitConnection)
   144  		}
   145  
   146  		connect = true
   147  		notifyClose := make(chan *amqp.Error)
   148  		r.Connection.NotifyClose(notifyClose)
   149  		chanNotifyClose := make(chan *amqp.Error)
   150  		var channel *amqp.Channel
   151  		if !r.withoutExchange {
   152  			channel = r.ExchangeChannel.channel
   153  		} else {
   154  			channel = r.Channel.channel
   155  		}
   156  		channel.NotifyClose(chanNotifyClose)
   157  		// To avoid deadlocks it is necessary to consume the messages from all channels.
   158  		for notifyClose != nil || chanNotifyClose != nil {
   159  			// block until closed
   160  			select {
   161  			case err := <-chanNotifyClose:
   162  				r.logger.Log(logger.ErrorLevel, err)
   163  				// block all resubscribe attempt - they are useless because there is no connection to rabbitmq
   164  				// create channel 'waitConnection' (at this point channel is nil or closed, create it without unnecessary checks)
   165  				r.Lock()
   166  				r.connected = false
   167  				r.waitConnection = make(chan struct{})
   168  				r.Unlock()
   169  				chanNotifyClose = nil
   170  			case err := <-notifyClose:
   171  				r.logger.Log(logger.ErrorLevel, err)
   172  				// block all resubscribe attempt - they are useless because there is no connection to rabbitmq
   173  				// create channel 'waitConnection' (at this point channel is nil or closed, create it without unnecessary checks)
   174  				r.Lock()
   175  				r.connected = false
   176  				r.waitConnection = make(chan struct{})
   177  				r.Unlock()
   178  				notifyClose = nil
   179  			case <-r.close:
   180  				return
   181  			}
   182  		}
   183  	}
   184  }
   185  
   186  func (r *rabbitMQConn) Connect(secure bool, config *amqp.Config) error {
   187  	r.Lock()
   188  
   189  	// already connected
   190  	if r.connected {
   191  		r.Unlock()
   192  		return nil
   193  	}
   194  
   195  	// check it was closed
   196  	select {
   197  	case <-r.close:
   198  		r.close = make(chan bool)
   199  	default:
   200  		// no op
   201  		// new conn
   202  	}
   203  
   204  	r.Unlock()
   205  
   206  	return r.connect(secure, config)
   207  }
   208  
   209  func (r *rabbitMQConn) Close() error {
   210  	r.Lock()
   211  	defer r.Unlock()
   212  
   213  	select {
   214  	case <-r.close:
   215  		return nil
   216  	default:
   217  		close(r.close)
   218  		r.connected = false
   219  	}
   220  
   221  	return r.Connection.Close()
   222  }
   223  
   224  func (r *rabbitMQConn) tryConnect(secure bool, config *amqp.Config) error {
   225  	var err error
   226  
   227  	if config == nil {
   228  		config = &defaultAmqpConfig
   229  	}
   230  
   231  	url := r.url
   232  
   233  	if secure || config.TLSClientConfig != nil || strings.HasPrefix(r.url, "amqps://") {
   234  		if config.TLSClientConfig == nil {
   235  			config.TLSClientConfig = &tls.Config{
   236  				InsecureSkipVerify: true,
   237  			}
   238  		}
   239  
   240  		url = strings.Replace(r.url, "amqp://", "amqps://", 1)
   241  	}
   242  
   243  	r.Connection, err = dialConfig(url, *config)
   244  
   245  	if err != nil {
   246  		return err
   247  	}
   248  
   249  	if r.Channel, err = newRabbitChannel(r.Connection, r.prefetchCount, r.prefetchGlobal, r.confirmPublish); err != nil {
   250  		return err
   251  	}
   252  
   253  	if !r.withoutExchange {
   254  		if r.exchange.Durable {
   255  			r.Channel.DeclareDurableExchange(r.exchange)
   256  		} else {
   257  			r.Channel.DeclareExchange(r.exchange)
   258  		}
   259  		r.ExchangeChannel, err = newRabbitChannel(r.Connection, r.prefetchCount, r.prefetchGlobal, r.confirmPublish)
   260  	}
   261  	return err
   262  }
   263  
   264  func (r *rabbitMQConn) Consume(queue, key string, headers amqp.Table, qArgs amqp.Table, autoAck, durableQueue bool) (*rabbitMQChannel, <-chan amqp.Delivery, error) {
   265  	consumerChannel, err := newRabbitChannel(r.Connection, r.prefetchCount, r.prefetchGlobal, r.confirmPublish)
   266  	if err != nil {
   267  		return nil, nil, err
   268  	}
   269  
   270  	if durableQueue {
   271  		err = consumerChannel.DeclareDurableQueue(queue, qArgs)
   272  	} else {
   273  		err = consumerChannel.DeclareQueue(queue, qArgs)
   274  	}
   275  
   276  	if err != nil {
   277  		return nil, nil, err
   278  	}
   279  
   280  	deliveries, err := consumerChannel.ConsumeQueue(queue, autoAck)
   281  	if err != nil {
   282  		return nil, nil, err
   283  	}
   284  
   285  	if !r.withoutExchange {
   286  		err = consumerChannel.BindQueue(queue, key, r.exchange.Name, headers)
   287  		if err != nil {
   288  			return nil, nil, err
   289  		}
   290  	}
   291  
   292  	return consumerChannel, deliveries, nil
   293  }
   294  
   295  func (r *rabbitMQConn) Publish(exchange, key string, msg amqp.Publishing) error {
   296  	if r.withoutExchange {
   297  		return r.Channel.Publish("", key, msg)
   298  	}
   299  	return r.ExchangeChannel.Publish(exchange, key, msg)
   300  }