package channelmanager import ( "errors" "sync" "time" amqp "github.com/rabbitmq/amqp091-go" "github.com/wagslane/go-rabbitmq/internal/connectionmanager" "github.com/wagslane/go-rabbitmq/internal/dispatcher" "github.com/wagslane/go-rabbitmq/internal/logger" ) // ChannelManager - type ChannelManager struct { logger logger.Logger channel *amqp.Channel connManager *connectionmanager.ConnectionManager channelMu *sync.RWMutex reconnectInterval time.Duration reconnectionCount uint reconnectionCountMu *sync.Mutex dispatcher *dispatcher.Dispatcher } // NewChannelManager creates a new connection manager func NewChannelManager(connManager *connectionmanager.ConnectionManager, log logger.Logger, reconnectInterval time.Duration) (*ChannelManager, error) { ch, err := getNewChannel(connManager) if err != nil { return nil, err } chanManager := ChannelManager{ logger: log, connManager: connManager, channel: ch, channelMu: &sync.RWMutex{}, reconnectInterval: reconnectInterval, reconnectionCount: 0, reconnectionCountMu: &sync.Mutex{}, dispatcher: dispatcher.NewDispatcher(), } go chanManager.startNotifyCancelOrClosed() return &chanManager, nil } func getNewChannel(connManager *connectionmanager.ConnectionManager) (*amqp.Channel, error) { conn := connManager.CheckoutConnection() defer connManager.CheckinConnection() ch, err := conn.Channel() if err != nil { return nil, err } return ch, nil } // startNotifyCancelOrClosed listens on the channel's cancelled and closed // notifiers. When it detects a problem, it attempts to reconnect. // Once reconnected, it sends an error back on the manager's notifyCancelOrClose // channel func (chanManager *ChannelManager) startNotifyCancelOrClosed() { notifyCloseChan := chanManager.channel.NotifyClose(make(chan *amqp.Error, 1)) notifyCancelChan := chanManager.channel.NotifyCancel(make(chan string, 1)) select { case err := <-notifyCloseChan: if err != nil { chanManager.logger.Errorf("attempting to reconnect to amqp server after close with error: %v", err) chanManager.reconnectLoop() chanManager.logger.Warnf("successfully reconnected to amqp server") chanManager.dispatcher.Dispatch(err) } if err == nil { chanManager.logger.Infof("amqp channel closed gracefully") } case err := <-notifyCancelChan: chanManager.logger.Errorf("attempting to reconnect to amqp server after cancel with error: %s", err) chanManager.reconnectLoop() chanManager.logger.Warnf("successfully reconnected to amqp server after cancel") chanManager.dispatcher.Dispatch(errors.New(err)) } } // GetReconnectionCount - func (chanManager *ChannelManager) GetReconnectionCount() uint { chanManager.reconnectionCountMu.Lock() defer chanManager.reconnectionCountMu.Unlock() return chanManager.reconnectionCount } func (chanManager *ChannelManager) incrementReconnectionCount() { chanManager.reconnectionCountMu.Lock() defer chanManager.reconnectionCountMu.Unlock() chanManager.reconnectionCount++ } // reconnectLoop continuously attempts to reconnect func (chanManager *ChannelManager) reconnectLoop() { for { chanManager.logger.Infof("waiting %s seconds to attempt to reconnect to amqp server", chanManager.reconnectInterval) time.Sleep(chanManager.reconnectInterval) err := chanManager.reconnect() if err != nil { chanManager.logger.Errorf("error reconnecting to amqp server: %v", err) } else { chanManager.incrementReconnectionCount() go chanManager.startNotifyCancelOrClosed() return } } } // reconnect safely closes the current channel and obtains a new one func (chanManager *ChannelManager) reconnect() error { chanManager.channelMu.Lock() defer chanManager.channelMu.Unlock() newChannel, err := getNewChannel(chanManager.connManager) if err != nil { return err } if err = chanManager.channel.Close(); err != nil { chanManager.logger.Warnf("error closing channel while reconnecting: %v", err) } chanManager.channel = newChannel return nil } // Close safely closes the current channel and connection func (chanManager *ChannelManager) Close() error { chanManager.logger.Infof("closing channel manager...") chanManager.channelMu.Lock() defer chanManager.channelMu.Unlock() err := chanManager.channel.Close() if err != nil { return err } return nil } // NotifyReconnect adds a new subscriber that will receive error messages whenever // the connection manager has successfully reconnect to the server func (chanManager *ChannelManager) NotifyReconnect() (<-chan error, chan<- struct{}) { return chanManager.dispatcher.AddSubscriber() }