| @ -1,134 +0,0 @@ | |||
| package rabbitmq | |||
| import ( | |||
| "errors" | |||
| "sync" | |||
| "time" | |||
| amqp "github.com/rabbitmq/amqp091-go" | |||
| ) | |||
| type channelManager struct { | |||
| logger Logger | |||
| url string | |||
| channel *amqp.Channel | |||
| connection *amqp.Connection | |||
| amqpConfig Config | |||
| channelMux *sync.RWMutex | |||
| notifyCancelOrClose chan error | |||
| reconnectInterval time.Duration | |||
| reconnectionCount uint | |||
| } | |||
| func newChannelManager(url string, conf Config, log Logger, reconnectInterval time.Duration) (*channelManager, error) { | |||
| conn, ch, err := getNewChannel(url, conf) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| chManager := channelManager{ | |||
| logger: log, | |||
| url: url, | |||
| connection: conn, | |||
| channel: ch, | |||
| channelMux: &sync.RWMutex{}, | |||
| amqpConfig: conf, | |||
| notifyCancelOrClose: make(chan error), | |||
| reconnectInterval: reconnectInterval, | |||
| } | |||
| go chManager.startNotifyCancelOrClosed() | |||
| return &chManager, nil | |||
| } | |||
| func getNewChannel(url string, conf Config) (*amqp.Connection, *amqp.Channel, error) { | |||
| amqpConn, err := amqp.DialConfig(url, amqp.Config(conf)) | |||
| if err != nil { | |||
| return nil, nil, err | |||
| } | |||
| ch, err := amqpConn.Channel() | |||
| if err != nil { | |||
| return nil, nil, err | |||
| } | |||
| return amqpConn, 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 (chManager *channelManager) startNotifyCancelOrClosed() { | |||
| notifyCloseChan := chManager.channel.NotifyClose(make(chan *amqp.Error, 1)) | |||
| notifyCancelChan := chManager.channel.NotifyCancel(make(chan string, 1)) | |||
| select { | |||
| case err := <-notifyCloseChan: | |||
| if err != nil { | |||
| chManager.logger.Errorf("attempting to reconnect to amqp server after close with error: %v", err) | |||
| chManager.reconnectLoop() | |||
| chManager.logger.Warnf("successfully reconnected to amqp server") | |||
| chManager.notifyCancelOrClose <- err | |||
| } | |||
| if err == nil { | |||
| chManager.logger.Infof("amqp channel closed gracefully") | |||
| } | |||
| case err := <-notifyCancelChan: | |||
| chManager.logger.Errorf("attempting to reconnect to amqp server after cancel with error: %s", err) | |||
| chManager.reconnectLoop() | |||
| chManager.logger.Warnf("successfully reconnected to amqp server after cancel") | |||
| chManager.notifyCancelOrClose <- errors.New(err) | |||
| } | |||
| } | |||
| // reconnectLoop continuously attempts to reconnect | |||
| func (chManager *channelManager) reconnectLoop() { | |||
| for { | |||
| chManager.logger.Infof("waiting %s seconds to attempt to reconnect to amqp server", chManager.reconnectInterval) | |||
| time.Sleep(chManager.reconnectInterval) | |||
| err := chManager.reconnect() | |||
| if err != nil { | |||
| chManager.logger.Errorf("error reconnecting to amqp server: %v", err) | |||
| } else { | |||
| chManager.reconnectionCount++ | |||
| go chManager.startNotifyCancelOrClosed() | |||
| return | |||
| } | |||
| } | |||
| } | |||
| // reconnect safely closes the current channel and obtains a new one | |||
| func (chManager *channelManager) reconnect() error { | |||
| chManager.channelMux.Lock() | |||
| defer chManager.channelMux.Unlock() | |||
| newConn, newChannel, err := getNewChannel(chManager.url, chManager.amqpConfig) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| if err = chManager.channel.Close(); err != nil { | |||
| chManager.logger.Warnf("error closing channel while reconnecting: %v", err) | |||
| } | |||
| if err = chManager.connection.Close(); err != nil { | |||
| chManager.logger.Warnf("error closing connection while reconnecting: %v", err) | |||
| } | |||
| chManager.connection = newConn | |||
| chManager.channel = newChannel | |||
| return nil | |||
| } | |||
| // close safely closes the current channel and connection | |||
| func (chManager *channelManager) close() error { | |||
| chManager.channelMux.Lock() | |||
| defer chManager.channelMux.Unlock() | |||
| err := chManager.channel.Close() | |||
| if err != nil { | |||
| return err | |||
| } | |||
| err = chManager.connection.Close() | |||
| if err != nil { | |||
| return err | |||
| } | |||
| return nil | |||
| } | |||
| @ -1,9 +0,0 @@ | |||
| package rabbitmq | |||
| import amqp "github.com/rabbitmq/amqp091-go" | |||
| // Config wraps amqp.Config | |||
| // Config is used in DialConfig and Open to specify the desired tuning | |||
| // parameters used during a connection open handshake. The negotiated tuning | |||
| // will be stored in the returned connection's Config field. | |||
| type Config amqp.Config | |||
| @ -0,0 +1,110 @@ | |||
| package rabbitmq | |||
| import ( | |||
| amqp "github.com/rabbitmq/amqp091-go" | |||
| "github.com/wagslane/go-rabbitmq/internal/connectionmanager" | |||
| ) | |||
| // Conn manages the connection to a rabbit cluster | |||
| // it is intended to be shared across publishers and consumers | |||
| type Conn struct { | |||
| connectionManager *connectionmanager.ConnectionManager | |||
| reconnectErrCh <-chan error | |||
| closeConnectionToManagerCh chan<- struct{} | |||
| notifyReturnChan chan Return | |||
| notifyPublishChan chan Confirmation | |||
| options ConnectionOptions | |||
| } | |||
| // Config wraps amqp.Config | |||
| // Config is used in DialConfig and Open to specify the desired tuning | |||
| // parameters used during a connection open handshake. The negotiated tuning | |||
| // will be stored in the returned connection's Config field. | |||
| type Config amqp.Config | |||
| // NewConn creates a new connection manager | |||
| func NewConn(url string, optionFuncs ...func(*ConnectionOptions)) (*Conn, error) { | |||
| defaultOptions := getDefaultConnectionOptions() | |||
| options := &defaultOptions | |||
| for _, optionFunc := range optionFuncs { | |||
| optionFunc(options) | |||
| } | |||
| manager, err := connectionmanager.NewConnectionManager(url, amqp.Config(options.Config), options.Logger, options.ReconnectInterval) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| err = manager.QosSafe( | |||
| options.QOSPrefetch, | |||
| 0, | |||
| options.QOSGlobal, | |||
| ) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| reconnectErrCh, closeCh := manager.NotifyReconnect() | |||
| conn := &Conn{ | |||
| connectionManager: manager, | |||
| reconnectErrCh: reconnectErrCh, | |||
| closeConnectionToManagerCh: closeCh, | |||
| notifyReturnChan: nil, | |||
| notifyPublishChan: nil, | |||
| options: *options, | |||
| } | |||
| go conn.handleRestarts() | |||
| return conn, nil | |||
| } | |||
| func (conn *Conn) handleRestarts() { | |||
| for err := range conn.reconnectErrCh { | |||
| conn.options.Logger.Infof("successful connection recovery from: %v", err) | |||
| go conn.startNotifyReturnHandler() | |||
| go conn.startNotifyPublishHandler() | |||
| } | |||
| } | |||
| func (conn *Conn) startNotifyReturnHandler() { | |||
| if conn.notifyReturnChan == nil { | |||
| return | |||
| } | |||
| returnAMQPCh := conn.connectionManager.NotifyReturnSafe(make(chan amqp.Return, 1)) | |||
| for ret := range returnAMQPCh { | |||
| conn.notifyReturnChan <- Return{ret} | |||
| } | |||
| } | |||
| func (conn *Conn) startNotifyPublishHandler() { | |||
| if conn.notifyPublishChan == nil { | |||
| return | |||
| } | |||
| conn.connectionManager.ConfirmSafe(false) | |||
| publishAMQPCh := conn.connectionManager.NotifyPublishSafe(make(chan amqp.Confirmation, 1)) | |||
| for conf := range publishAMQPCh { | |||
| conn.notifyPublishChan <- Confirmation{ | |||
| Confirmation: conf, | |||
| ReconnectionCount: int(conn.connectionManager.GetReconnectionCount()), | |||
| } | |||
| } | |||
| } | |||
| // NotifyReturn registers a listener for basic.return methods. | |||
| // These can be sent from the server when a publish is undeliverable either from the mandatory or immediate flags. | |||
| // These notifications are shared across an entire connection, so if you're creating multiple | |||
| // publishers on the same connection keep that in mind | |||
| func (conn *Conn) NotifyReturn() <-chan Return { | |||
| conn.notifyReturnChan = make(chan Return) | |||
| go conn.startNotifyReturnHandler() | |||
| return conn.notifyReturnChan | |||
| } | |||
| // NotifyPublish registers a listener for publish confirmations, must set ConfirmPublishings option | |||
| // These notifications are shared across an entire connection, so if you're creating multiple | |||
| // publishers on the same connection keep that in mind | |||
| func (conn *Conn) NotifyPublish() <-chan Confirmation { | |||
| conn.notifyPublishChan = make(chan Confirmation) | |||
| go conn.startNotifyPublishHandler() | |||
| return conn.notifyPublishChan | |||
| } | |||
| @ -0,0 +1,67 @@ | |||
| package rabbitmq | |||
| import "time" | |||
| // ConnectionOptions are used to describe how a new consumer will be created. | |||
| type ConnectionOptions struct { | |||
| QOSPrefetch int | |||
| QOSGlobal bool | |||
| ReconnectInterval time.Duration | |||
| Logger Logger | |||
| Config Config | |||
| } | |||
| // getDefaultConnectionOptions describes the options that will be used when a value isn't provided | |||
| func getDefaultConnectionOptions() ConnectionOptions { | |||
| return ConnectionOptions{ | |||
| QOSPrefetch: 0, | |||
| QOSGlobal: false, | |||
| ReconnectInterval: time.Second * 5, | |||
| Logger: stdDebugLogger{}, | |||
| Config: Config{}, | |||
| } | |||
| } | |||
| // WithConnectionOptionsQOSPrefetch returns a function that sets the prefetch count, which means that | |||
| // many messages will be fetched from the server in advance to help with throughput. | |||
| // This doesn't affect the handler, messages are still processed one at a time. | |||
| func WithConnectionOptionsQOSPrefetch(prefetchCount int) func(*ConnectionOptions) { | |||
| return func(options *ConnectionOptions) { | |||
| options.QOSPrefetch = prefetchCount | |||
| } | |||
| } | |||
| // WithConnectionOptionsQOSGlobal sets the qos on the channel to global, which means | |||
| // these QOS settings apply to ALL existing and future | |||
| // consumers on all channels on the same connection | |||
| func WithConnectionOptionsQOSGlobal(options *ConnectionOptions) { | |||
| options.QOSGlobal = true | |||
| } | |||
| // WithConnectionOptionsReconnectInterval sets the reconnection interval | |||
| func WithConnectionOptionsReconnectInterval(interval time.Duration) func(options *ConnectionOptions) { | |||
| return func(options *ConnectionOptions) { | |||
| options.ReconnectInterval = interval | |||
| } | |||
| } | |||
| // WithConnectionOptionsLogging sets logging to true on the consumer options | |||
| // and sets the | |||
| func WithConnectionOptionsLogging(options *ConnectionOptions) { | |||
| options.Logger = stdDebugLogger{} | |||
| } | |||
| // WithConnectionOptionsLogger sets logging to true on the consumer options | |||
| // and sets the | |||
| func WithConnectionOptionsLogger(log Logger) func(options *ConnectionOptions) { | |||
| return func(options *ConnectionOptions) { | |||
| options.Logger = log | |||
| } | |||
| } | |||
| // WithConnectionOptionsConfig sets the Config used in the connection | |||
| func WithConnectionOptionsConfig(cfg Config) func(options *ConnectionOptions) { | |||
| return func(options *ConnectionOptions) { | |||
| options.Config = cfg | |||
| } | |||
| } | |||
| @ -1,373 +1,90 @@ | |||
| package rabbitmq | |||
| import "fmt" | |||
| // DeclareOptions are used to describe how a new queues, exchanges the routing setup should look like. | |||
| type DeclareOptions struct { | |||
| Queue *QueueOptions | |||
| Exchange *ExchangeOptions | |||
| Bindings []Binding | |||
| } | |||
| // QueueOptions are used to configure a queue. | |||
| // If the Passive flag is set the client will only check if the queue exists on the server | |||
| // and that the settings match, no creation attempt will be made. | |||
| type QueueOptions struct { | |||
| Name string | |||
| Durable bool | |||
| AutoDelete bool | |||
| Exclusive bool | |||
| NoWait bool | |||
| Passive bool // if false, a missing queue will be created on the server | |||
| Args Table | |||
| } | |||
| // ExchangeOptions are used to configure an exchange. | |||
| // If the Passive flag is set the client will only check if the exchange exists on the server | |||
| // and that the settings match, no creation attempt will be made. | |||
| type ExchangeOptions struct { | |||
| Name string | |||
| Kind string // possible values: empty string for default exchange or direct, topic, fanout | |||
| Durable bool | |||
| AutoDelete bool | |||
| Internal bool | |||
| NoWait bool | |||
| Passive bool // if false, a missing exchange will be created on the server | |||
| Args Table | |||
| } | |||
| // BindingOption are used to configure a queue bindings. | |||
| type BindingOption struct { | |||
| NoWait bool | |||
| Args Table | |||
| } | |||
| // Binding describes a queue binding to a specific exchange. | |||
| type Binding struct { | |||
| BindingOption | |||
| QueueName string | |||
| ExchangeName string | |||
| RoutingKey string | |||
| } | |||
| // SetBindings trys to generate bindings for the given routing keys and the queue and exchange options. | |||
| // If either Queue or Exchange properties are empty or no queue name is specified, no bindings will be set. | |||
| func (o *DeclareOptions) SetBindings(routingKeys []string, opt BindingOption) { | |||
| if o.Queue == nil || o.Exchange == nil { | |||
| return // nothing to set... | |||
| } | |||
| if o.Queue.Name == "" { | |||
| return // nothing to set... | |||
| } | |||
| for _, routingKey := range routingKeys { | |||
| o.Bindings = append(o.Bindings, Binding{ | |||
| QueueName: o.Queue.Name, | |||
| ExchangeName: o.Exchange.Name, | |||
| RoutingKey: routingKey, | |||
| BindingOption: opt, | |||
| }) | |||
| } | |||
| } | |||
| // handleDeclare handles the queue, exchange and binding declare process on the server. | |||
| // If there are no options set, no actions will be executed. | |||
| func handleDeclare(chManager *channelManager, options DeclareOptions) error { | |||
| chManager.channelMux.RLock() | |||
| defer chManager.channelMux.RUnlock() | |||
| // bind queue | |||
| if options.Queue != nil { | |||
| queue := options.Queue | |||
| if queue.Name == "" { | |||
| return fmt.Errorf("missing queue name") | |||
| } | |||
| if queue.Passive { | |||
| _, err := chManager.channel.QueueDeclarePassive( | |||
| queue.Name, | |||
| queue.Durable, | |||
| queue.AutoDelete, | |||
| queue.Exclusive, | |||
| queue.NoWait, | |||
| tableToAMQPTable(queue.Args), | |||
| ) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| } else { | |||
| _, err := chManager.channel.QueueDeclare( | |||
| queue.Name, | |||
| queue.Durable, | |||
| queue.AutoDelete, | |||
| queue.Exclusive, | |||
| queue.NoWait, | |||
| tableToAMQPTable(queue.Args), | |||
| ) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| } | |||
| } | |||
| // bind exchange | |||
| if options.Exchange != nil { | |||
| exchange := options.Exchange | |||
| if exchange.Name == "" { | |||
| return fmt.Errorf("missing exchange name") | |||
| } | |||
| if exchange.Passive { | |||
| err := chManager.channel.ExchangeDeclarePassive( | |||
| exchange.Name, | |||
| exchange.Kind, | |||
| exchange.Durable, | |||
| exchange.AutoDelete, | |||
| exchange.Internal, | |||
| exchange.NoWait, | |||
| tableToAMQPTable(exchange.Args), | |||
| ) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| } else { | |||
| err := chManager.channel.ExchangeDeclare( | |||
| exchange.Name, | |||
| exchange.Kind, | |||
| exchange.Durable, | |||
| exchange.AutoDelete, | |||
| exchange.Internal, | |||
| exchange.NoWait, | |||
| tableToAMQPTable(exchange.Args), | |||
| ) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| } | |||
| } | |||
| // handle binding of queues to exchange | |||
| for _, binding := range options.Bindings { | |||
| err := chManager.channel.QueueBind( | |||
| binding.QueueName, // name of the queue | |||
| binding.RoutingKey, // bindingKey | |||
| binding.ExchangeName, // sourceExchange | |||
| binding.NoWait, // noWait | |||
| tableToAMQPTable(binding.Args), // arguments | |||
| import ( | |||
| "github.com/wagslane/go-rabbitmq/internal/connectionmanager" | |||
| ) | |||
| func declareQueue(connManager *connectionmanager.ConnectionManager, options QueueOptions) error { | |||
| if !options.Declare { | |||
| return nil | |||
| } | |||
| if options.Passive { | |||
| _, err := connManager.QueueDeclarePassiveSafe( | |||
| options.Name, | |||
| options.Durable, | |||
| options.AutoDelete, | |||
| options.Exclusive, | |||
| options.NoWait, | |||
| tableToAMQPTable(options.Args), | |||
| ) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| return nil | |||
| } | |||
| _, err := connManager.QueueDeclareSafe( | |||
| options.Name, | |||
| options.Durable, | |||
| options.AutoDelete, | |||
| options.Exclusive, | |||
| options.NoWait, | |||
| tableToAMQPTable(options.Args), | |||
| ) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| return nil | |||
| } | |||
| // getExchangeOptionsOrSetDefault returns pointer to current ExchangeOptions options. | |||
| // If no exchange options are set yet, new options with default values will be defined. | |||
| func getExchangeOptionsOrSetDefault(options *DeclareOptions) *ExchangeOptions { | |||
| if options.Exchange == nil { | |||
| options.Exchange = &ExchangeOptions{ | |||
| Name: "", | |||
| Kind: "direct", | |||
| Durable: false, | |||
| AutoDelete: false, | |||
| Internal: false, | |||
| NoWait: false, | |||
| Args: nil, | |||
| Passive: false, | |||
| } | |||
| func declareExchange(connManager *connectionmanager.ConnectionManager, options ExchangeOptions) error { | |||
| if !options.Declare { | |||
| return nil | |||
| } | |||
| return options.Exchange | |||
| } | |||
| // getQueueOptionsOrSetDefault returns pointer to current QueueOptions options. | |||
| // If no queue options are set yet, new options with default values will be defined. | |||
| func getQueueOptionsOrSetDefault(options *DeclareOptions) *QueueOptions { | |||
| if options.Queue == nil { | |||
| options.Queue = &QueueOptions{ | |||
| Name: "", | |||
| Durable: false, | |||
| AutoDelete: false, | |||
| Exclusive: false, | |||
| NoWait: false, | |||
| Passive: false, | |||
| Args: nil, | |||
| if options.Passive { | |||
| err := connManager.ExchangeDeclarePassiveSafe( | |||
| options.Name, | |||
| options.Kind, | |||
| options.Durable, | |||
| options.AutoDelete, | |||
| options.Internal, | |||
| options.NoWait, | |||
| tableToAMQPTable(options.Args), | |||
| ) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| return nil | |||
| } | |||
| err := connManager.ExchangeDeclareSafe( | |||
| options.Name, | |||
| options.Kind, | |||
| options.Durable, | |||
| options.AutoDelete, | |||
| options.Internal, | |||
| options.NoWait, | |||
| tableToAMQPTable(options.Args), | |||
| ) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| return options.Queue | |||
| } | |||
| // region general-options | |||
| // WithDeclareQueue sets the queue that should be declared prior to other RabbitMQ actions are being executed. | |||
| // Only the settings will be validated if the queue already exists on the server. | |||
| // Matching settings will result in no action, different settings will result in an error. | |||
| // If the 'Passive' property is set to false, a missing queue will be created on the server. | |||
| func WithDeclareQueue(settings *QueueOptions) func(*DeclareOptions) { | |||
| return func(options *DeclareOptions) { | |||
| options.Queue = settings | |||
| } | |||
| } | |||
| // WithDeclareExchange sets the exchange that should be declared prior to other RabbitMQ actions are being executed. | |||
| // Only the settings will be validated if the exchange already exists on the server. | |||
| // Matching settings will result in no action, different settings will result in an error. | |||
| // If the 'Passive' property is set to false, a missing exchange will be created on the server. | |||
| func WithDeclareExchange(settings *ExchangeOptions) func(*DeclareOptions) { | |||
| return func(options *DeclareOptions) { | |||
| options.Exchange = settings | |||
| } | |||
| } | |||
| // WithDeclareBindings sets the bindings that should be declared prior to other RabbitMQ actions are being executed. | |||
| // Only the settings will be validated if one of the bindings already exists on the server. | |||
| // Matching settings will result in no action, different settings will result in an error. | |||
| // If the 'Passive' property is set to false, missing bindings will be created on the server. | |||
| func WithDeclareBindings(bindings []Binding) func(*DeclareOptions) { | |||
| return func(options *DeclareOptions) { | |||
| options.Bindings = bindings | |||
| } | |||
| } | |||
| // WithDeclareBindingsForRoutingKeys sets the bindings that should be declared prior to other RabbitMQ | |||
| // actions are being executed. | |||
| // This function must be called after the queue and exchange declaration settings have been set, | |||
| // otherwise this function has no effect. | |||
| func WithDeclareBindingsForRoutingKeys(routingKeys []string) func(*DeclareOptions) { | |||
| return func(options *DeclareOptions) { | |||
| options.SetBindings(routingKeys, BindingOption{}) | |||
| } | |||
| } | |||
| // endregion general-options | |||
| // region single-options | |||
| // WithDeclareQueueName returns a function that sets the queue name. | |||
| func WithDeclareQueueName(name string) func(*DeclareOptions) { | |||
| return func(options *DeclareOptions) { | |||
| getQueueOptionsOrSetDefault(options).Name = name | |||
| } | |||
| } | |||
| // WithDeclareQueueDurable sets the queue to durable, which means it won't | |||
| // be destroyed when the server restarts. It must only be bound to durable exchanges. | |||
| func WithDeclareQueueDurable(options *DeclareOptions) { | |||
| getQueueOptionsOrSetDefault(options).Durable = true | |||
| } | |||
| // WithDeclareQueueAutoDelete sets the queue to auto delete, which means it will | |||
| // be deleted when there are no more consumers on it. | |||
| func WithDeclareQueueAutoDelete(options *DeclareOptions) { | |||
| getQueueOptionsOrSetDefault(options).AutoDelete = true | |||
| } | |||
| // WithDeclareQueueExclusive sets the queue to exclusive, which means | |||
| // it's are only accessible by the connection that declares it and | |||
| // will be deleted when the connection closes. Channels on other connections | |||
| // will receive an error when attempting to declare, bind, consume, purge or | |||
| // delete a queue with the same name. | |||
| func WithDeclareQueueExclusive(options *DeclareOptions) { | |||
| getQueueOptionsOrSetDefault(options).Exclusive = true | |||
| } | |||
| // WithDeclareQueueNoWait sets the queue to nowait, which means | |||
| // the queue will assume to be declared on the server. A channel | |||
| // exception will arrive if the conditions are met for existing queues | |||
| // or attempting to modify an existing queue from a different connection. | |||
| func WithDeclareQueueNoWait(options *DeclareOptions) { | |||
| getQueueOptionsOrSetDefault(options).NoWait = true | |||
| } | |||
| // WithDeclareQueueNoDeclare sets the queue to no declare, which means | |||
| // the queue will be assumed to be declared on the server, and thus only will be validated. | |||
| func WithDeclareQueueNoDeclare(options *DeclareOptions) { | |||
| getQueueOptionsOrSetDefault(options).Passive = true | |||
| } | |||
| // WithDeclareQueueArgs returns a function that sets the queue arguments. | |||
| func WithDeclareQueueArgs(args Table) func(*DeclareOptions) { | |||
| return func(options *DeclareOptions) { | |||
| getQueueOptionsOrSetDefault(options).Args = args | |||
| } | |||
| } | |||
| // WithDeclareQueueQuorum sets the queue a quorum type, which means multiple nodes | |||
| // in the cluster will have the messages distributed amongst them for higher reliability. | |||
| func WithDeclareQueueQuorum(options *DeclareOptions) { | |||
| queue := getQueueOptionsOrSetDefault(options) | |||
| if queue.Args == nil { | |||
| queue.Args = Table{} | |||
| } | |||
| queue.Args["x-queue-type"] = "quorum" | |||
| } | |||
| // WithDeclareExchangeName returns a function that sets the exchange name. | |||
| func WithDeclareExchangeName(name string) func(*DeclareOptions) { | |||
| return func(options *DeclareOptions) { | |||
| getExchangeOptionsOrSetDefault(options).Name = name | |||
| } | |||
| } | |||
| // WithDeclareExchangeKind returns a function that sets the binding exchange kind/type. | |||
| func WithDeclareExchangeKind(kind string) func(*DeclareOptions) { | |||
| return func(options *DeclareOptions) { | |||
| getExchangeOptionsOrSetDefault(options).Kind = kind | |||
| } | |||
| } | |||
| // WithDeclareExchangeDurable returns a function that sets the binding exchange durable flag. | |||
| func WithDeclareExchangeDurable(options *DeclareOptions) { | |||
| getExchangeOptionsOrSetDefault(options).Durable = true | |||
| } | |||
| // WithDeclareExchangeAutoDelete returns a function that sets the binding exchange autoDelete flag. | |||
| func WithDeclareExchangeAutoDelete(options *DeclareOptions) { | |||
| getExchangeOptionsOrSetDefault(options).AutoDelete = true | |||
| } | |||
| // WithDeclareExchangeInternal returns a function that sets the binding exchange internal flag. | |||
| func WithDeclareExchangeInternal(options *DeclareOptions) { | |||
| getExchangeOptionsOrSetDefault(options).Internal = true | |||
| } | |||
| // WithDeclareExchangeNoWait returns a function that sets the binding exchange noWait flag. | |||
| func WithDeclareExchangeNoWait(options *DeclareOptions) { | |||
| getExchangeOptionsOrSetDefault(options).NoWait = true | |||
| } | |||
| // WithDeclareExchangeArgs returns a function that sets the binding exchange arguments | |||
| // that are specific to the server's implementation of the exchange. | |||
| func WithDeclareExchangeArgs(args Table) func(*DeclareOptions) { | |||
| return func(options *DeclareOptions) { | |||
| getExchangeOptionsOrSetDefault(options).Args = args | |||
| } | |||
| } | |||
| // WithDeclareExchangeNoDeclare returns a function that skips the declaration of the | |||
| // binding exchange. Use this setting if the exchange already exists and you don't need to declare | |||
| // it on consumer start. | |||
| func WithDeclareExchangeNoDeclare(options *DeclareOptions) { | |||
| getExchangeOptionsOrSetDefault(options).Passive = true | |||
| } | |||
| // WithDeclareBindingNoWait sets the bindings to nowait, which means if the queue can not be bound | |||
| // the channel will not be closed with an error. | |||
| // This function must be called after bindings have been defined, otherwise it has no effect. | |||
| func WithDeclareBindingNoWait(options *DeclareOptions) { | |||
| for i := range options.Bindings { | |||
| options.Bindings[i].NoWait = true | |||
| } | |||
| return nil | |||
| } | |||
| // WithDeclareBindingArgs sets the arguments of the bindings to args. | |||
| // This function must be called after bindings have been defined, otherwise it has no effect. | |||
| func WithDeclareBindingArgs(args Table) func(*DeclareOptions) { | |||
| return func(options *DeclareOptions) { | |||
| for i := range options.Bindings { | |||
| options.Bindings[i].Args = args | |||
| func declareBindings(connManager *connectionmanager.ConnectionManager, options ConsumeOptions) error { | |||
| for _, binding := range options.Bindings { | |||
| if !binding.Declare { | |||
| continue | |||
| } | |||
| err := connManager.QueueBindSafe( | |||
| options.QueueOptions.Name, | |||
| binding.RoutingKey, | |||
| options.ExchangeOptions.Name, | |||
| binding.NoWait, | |||
| tableToAMQPTable(binding.Args), | |||
| ) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| } | |||
| return nil | |||
| } | |||
| // endregion single-options | |||
| @ -1 +0,0 @@ | |||
| consumer_with_declare | |||
| @ -1,74 +0,0 @@ | |||
| package main | |||
| import ( | |||
| "fmt" | |||
| "log" | |||
| "os" | |||
| "os/signal" | |||
| "syscall" | |||
| rabbitmq "github.com/wagslane/go-rabbitmq" | |||
| ) | |||
| var consumerName = "example_with_declare" | |||
| func main() { | |||
| consumer, err := rabbitmq.NewConsumer( | |||
| "amqp://guest:guest@localhost", rabbitmq.Config{}, | |||
| rabbitmq.WithConsumerOptionsLogging, | |||
| ) | |||
| if err != nil { | |||
| log.Fatal(err) | |||
| } | |||
| defer func() { | |||
| err := consumer.Close() | |||
| if err != nil { | |||
| log.Fatal(err) | |||
| } | |||
| }() | |||
| err = consumer.StartConsuming( | |||
| func(d rabbitmq.Delivery) rabbitmq.Action { | |||
| log.Printf("consumed: %v", string(d.Body)) | |||
| // rabbitmq.Ack, rabbitmq.NackDiscard, rabbitmq.NackRequeue | |||
| return rabbitmq.Ack | |||
| }, | |||
| "my_queue", | |||
| rabbitmq.WithConsumeDeclareOptions( | |||
| rabbitmq.WithDeclareQueueDurable, | |||
| rabbitmq.WithDeclareQueueQuorum, | |||
| rabbitmq.WithDeclareExchangeName("events"), | |||
| rabbitmq.WithDeclareExchangeKind("topic"), | |||
| rabbitmq.WithDeclareExchangeDurable, | |||
| rabbitmq.WithDeclareBindingsForRoutingKeys([]string{"routing_key", "routing_key_2"}), // implicit bindings | |||
| rabbitmq.WithDeclareBindings([]rabbitmq.Binding{ // custom bindings | |||
| { | |||
| QueueName: "my_queue", | |||
| ExchangeName: "events", | |||
| RoutingKey: "a_custom_key", | |||
| }, | |||
| }), | |||
| ), | |||
| rabbitmq.WithConsumeOptionsConsumerName(consumerName), | |||
| ) | |||
| if err != nil { | |||
| log.Fatal(err) | |||
| } | |||
| // block main thread - wait for shutdown signal | |||
| sigs := make(chan os.Signal, 1) | |||
| done := make(chan bool, 1) | |||
| signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) | |||
| go func() { | |||
| sig := <-sigs | |||
| fmt.Println() | |||
| fmt.Println(sig) | |||
| done <- true | |||
| }() | |||
| fmt.Println("awaiting signal") | |||
| <-done | |||
| fmt.Println("stopping consumer") | |||
| } | |||
| @ -0,0 +1,16 @@ | |||
| package rabbitmq | |||
| // ExchangeOptions are used to configure an exchange. | |||
| // If the Passive flag is set the client will only check if the exchange exists on the server | |||
| // and that the settings match, no creation attempt will be made. | |||
| type ExchangeOptions struct { | |||
| Name string | |||
| Kind string // possible values: empty string for default exchange or direct, topic, fanout | |||
| Durable bool | |||
| AutoDelete bool | |||
| Internal bool | |||
| NoWait bool | |||
| Passive bool // if false, a missing exchange will be created on the server | |||
| Args Table | |||
| Declare bool | |||
| } | |||
| @ -0,0 +1,160 @@ | |||
| package connectionmanager | |||
| import ( | |||
| "errors" | |||
| "sync" | |||
| "time" | |||
| amqp "github.com/rabbitmq/amqp091-go" | |||
| "github.com/wagslane/go-rabbitmq/internal/logger" | |||
| ) | |||
| // ConnectionManager - | |||
| type ConnectionManager struct { | |||
| logger logger.Logger | |||
| url string | |||
| channel *amqp.Channel | |||
| connection *amqp.Connection | |||
| amqpConfig amqp.Config | |||
| channelMux *sync.RWMutex | |||
| reconnectInterval time.Duration | |||
| reconnectionCount uint | |||
| reconnectionCountMux *sync.Mutex | |||
| dispatcher *dispatcher | |||
| } | |||
| // NewConnectionManager creates a new connection manager | |||
| func NewConnectionManager(url string, conf amqp.Config, log logger.Logger, reconnectInterval time.Duration) (*ConnectionManager, error) { | |||
| conn, ch, err := getNewChannel(url, conf) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| connManager := ConnectionManager{ | |||
| logger: log, | |||
| url: url, | |||
| connection: conn, | |||
| channel: ch, | |||
| channelMux: &sync.RWMutex{}, | |||
| amqpConfig: conf, | |||
| reconnectInterval: reconnectInterval, | |||
| reconnectionCount: 0, | |||
| reconnectionCountMux: &sync.Mutex{}, | |||
| dispatcher: newDispatcher(), | |||
| } | |||
| go connManager.startNotifyCancelOrClosed() | |||
| return &connManager, nil | |||
| } | |||
| func getNewChannel(url string, conf amqp.Config) (*amqp.Connection, *amqp.Channel, error) { | |||
| amqpConn, err := amqp.DialConfig(url, amqp.Config(conf)) | |||
| if err != nil { | |||
| return nil, nil, err | |||
| } | |||
| ch, err := amqpConn.Channel() | |||
| if err != nil { | |||
| return nil, nil, err | |||
| } | |||
| return amqpConn, 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 (connManager *ConnectionManager) startNotifyCancelOrClosed() { | |||
| notifyCloseChan := connManager.channel.NotifyClose(make(chan *amqp.Error, 1)) | |||
| notifyCancelChan := connManager.channel.NotifyCancel(make(chan string, 1)) | |||
| select { | |||
| case err := <-notifyCloseChan: | |||
| if err != nil { | |||
| connManager.logger.Errorf("attempting to reconnect to amqp server after close with error: %v", err) | |||
| connManager.reconnectLoop() | |||
| connManager.logger.Warnf("successfully reconnected to amqp server") | |||
| connManager.dispatcher.dispatch(err) | |||
| } | |||
| if err == nil { | |||
| connManager.logger.Infof("amqp channel closed gracefully") | |||
| } | |||
| case err := <-notifyCancelChan: | |||
| connManager.logger.Errorf("attempting to reconnect to amqp server after cancel with error: %s", err) | |||
| connManager.reconnectLoop() | |||
| connManager.logger.Warnf("successfully reconnected to amqp server after cancel") | |||
| connManager.dispatcher.dispatch(errors.New(err)) | |||
| } | |||
| } | |||
| // GetReconnectionCount - | |||
| func (connManager *ConnectionManager) GetReconnectionCount() uint { | |||
| connManager.reconnectionCountMux.Lock() | |||
| defer connManager.reconnectionCountMux.Unlock() | |||
| return connManager.reconnectionCount | |||
| } | |||
| func (connManager *ConnectionManager) incrementReconnectionCount() { | |||
| connManager.reconnectionCountMux.Lock() | |||
| defer connManager.reconnectionCountMux.Unlock() | |||
| connManager.reconnectionCount++ | |||
| } | |||
| // reconnectLoop continuously attempts to reconnect | |||
| func (connManager *ConnectionManager) reconnectLoop() { | |||
| for { | |||
| connManager.logger.Infof("waiting %s seconds to attempt to reconnect to amqp server", connManager.reconnectInterval) | |||
| time.Sleep(connManager.reconnectInterval) | |||
| err := connManager.reconnect() | |||
| if err != nil { | |||
| connManager.logger.Errorf("error reconnecting to amqp server: %v", err) | |||
| } else { | |||
| connManager.incrementReconnectionCount() | |||
| go connManager.startNotifyCancelOrClosed() | |||
| return | |||
| } | |||
| } | |||
| } | |||
| // reconnect safely closes the current channel and obtains a new one | |||
| func (connManager *ConnectionManager) reconnect() error { | |||
| connManager.channelMux.Lock() | |||
| defer connManager.channelMux.Unlock() | |||
| newConn, newChannel, err := getNewChannel(connManager.url, connManager.amqpConfig) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| if err = connManager.channel.Close(); err != nil { | |||
| connManager.logger.Warnf("error closing channel while reconnecting: %v", err) | |||
| } | |||
| if err = connManager.connection.Close(); err != nil { | |||
| connManager.logger.Warnf("error closing connection while reconnecting: %v", err) | |||
| } | |||
| connManager.connection = newConn | |||
| connManager.channel = newChannel | |||
| return nil | |||
| } | |||
| // close safely closes the current channel and connection | |||
| func (connManager *ConnectionManager) close() error { | |||
| connManager.logger.Infof("closing connection manager...") | |||
| connManager.channelMux.Lock() | |||
| defer connManager.channelMux.Unlock() | |||
| err := connManager.channel.Close() | |||
| if err != nil { | |||
| return err | |||
| } | |||
| err = connManager.connection.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 (connManager *ConnectionManager) NotifyReconnect() (<-chan error, chan<- struct{}) { | |||
| return connManager.dispatcher.addSubscriber() | |||
| } | |||
| @ -0,0 +1,68 @@ | |||
| package connectionmanager | |||
| import ( | |||
| "log" | |||
| "math" | |||
| "math/rand" | |||
| "sync" | |||
| "time" | |||
| ) | |||
| type dispatcher struct { | |||
| subscribers map[int]dispatchSubscriber | |||
| subscribersMux *sync.Mutex | |||
| } | |||
| type dispatchSubscriber struct { | |||
| notifyCancelOrCloseChan chan error | |||
| closeCh <-chan struct{} | |||
| } | |||
| func newDispatcher() *dispatcher { | |||
| return &dispatcher{ | |||
| subscribers: make(map[int]dispatchSubscriber), | |||
| subscribersMux: &sync.Mutex{}, | |||
| } | |||
| } | |||
| func (d *dispatcher) dispatch(err error) error { | |||
| d.subscribersMux.Lock() | |||
| defer d.subscribersMux.Unlock() | |||
| for _, subscriber := range d.subscribers { | |||
| select { | |||
| case <-time.After(time.Second * 5): | |||
| log.Println("Unexpected rabbitmq error: timeout in dispatch") | |||
| case subscriber.notifyCancelOrCloseChan <- err: | |||
| } | |||
| } | |||
| return nil | |||
| } | |||
| func (d *dispatcher) addSubscriber() (<-chan error, chan<- struct{}) { | |||
| const maxRand = math.MaxInt64 | |||
| const minRand = 0 | |||
| id := rand.Intn(maxRand-minRand) + minRand | |||
| closeCh := make(chan struct{}) | |||
| notifyCancelOrCloseChan := make(chan error) | |||
| d.subscribersMux.Lock() | |||
| d.subscribers[id] = dispatchSubscriber{ | |||
| notifyCancelOrCloseChan: notifyCancelOrCloseChan, | |||
| closeCh: closeCh, | |||
| } | |||
| d.subscribersMux.Unlock() | |||
| go func(id int) { | |||
| <-closeCh | |||
| d.subscribersMux.Lock() | |||
| defer d.subscribersMux.Unlock() | |||
| sub, ok := d.subscribers[id] | |||
| if !ok { | |||
| return | |||
| } | |||
| close(sub.notifyCancelOrCloseChan) | |||
| delete(d.subscribers, id) | |||
| }(id) | |||
| return notifyCancelOrCloseChan, closeCh | |||
| } | |||
| @ -0,0 +1,210 @@ | |||
| package connectionmanager | |||
| import ( | |||
| amqp "github.com/rabbitmq/amqp091-go" | |||
| ) | |||
| // ConsumeSafe safely wraps the (*amqp.Channel).Consume method | |||
| func (connManager *ConnectionManager) ConsumeSafe( | |||
| queue, | |||
| consumer string, | |||
| autoAck, | |||
| exclusive, | |||
| noLocal, | |||
| noWait bool, | |||
| args amqp.Table, | |||
| ) (<-chan amqp.Delivery, error) { | |||
| connManager.channelMux.RLock() | |||
| defer connManager.channelMux.RUnlock() | |||
| return connManager.channel.Consume( | |||
| queue, | |||
| consumer, | |||
| autoAck, | |||
| exclusive, | |||
| noLocal, | |||
| noWait, | |||
| args, | |||
| ) | |||
| } | |||
| // QueueDeclarePassiveSafe safely wraps the (*amqp.Channel).QueueDeclarePassive method | |||
| func (connManager *ConnectionManager) QueueDeclarePassiveSafe( | |||
| name string, | |||
| durable bool, | |||
| autoDelete bool, | |||
| exclusive bool, | |||
| noWait bool, | |||
| args amqp.Table, | |||
| ) (amqp.Queue, error) { | |||
| connManager.channelMux.RLock() | |||
| defer connManager.channelMux.RUnlock() | |||
| return connManager.channel.QueueDeclarePassive( | |||
| name, | |||
| durable, | |||
| autoDelete, | |||
| exclusive, | |||
| noWait, | |||
| args, | |||
| ) | |||
| } | |||
| // QueueDeclareSafe safely wraps the (*amqp.Channel).QueueDeclare method | |||
| func (connManager *ConnectionManager) QueueDeclareSafe( | |||
| name string, durable bool, autoDelete bool, exclusive bool, noWait bool, args amqp.Table, | |||
| ) (amqp.Queue, error) { | |||
| connManager.channelMux.RLock() | |||
| defer connManager.channelMux.RUnlock() | |||
| return connManager.channel.QueueDeclare( | |||
| name, | |||
| durable, | |||
| autoDelete, | |||
| exclusive, | |||
| noWait, | |||
| args, | |||
| ) | |||
| } | |||
| // ExchangeDeclarePassiveSafe safely wraps the (*amqp.Channel).ExchangeDeclarePassive method | |||
| func (connManager *ConnectionManager) ExchangeDeclarePassiveSafe( | |||
| name string, kind string, durable bool, autoDelete bool, internal bool, noWait bool, args amqp.Table, | |||
| ) error { | |||
| connManager.channelMux.RLock() | |||
| defer connManager.channelMux.RUnlock() | |||
| return connManager.channel.ExchangeDeclarePassive( | |||
| name, | |||
| kind, | |||
| durable, | |||
| autoDelete, | |||
| internal, | |||
| noWait, | |||
| args, | |||
| ) | |||
| } | |||
| // ExchangeDeclareSafe safely wraps the (*amqp.Channel).ExchangeDeclare method | |||
| func (connManager *ConnectionManager) ExchangeDeclareSafe( | |||
| name string, kind string, durable bool, autoDelete bool, internal bool, noWait bool, args amqp.Table, | |||
| ) error { | |||
| connManager.channelMux.RLock() | |||
| defer connManager.channelMux.RUnlock() | |||
| return connManager.channel.ExchangeDeclare( | |||
| name, | |||
| kind, | |||
| durable, | |||
| autoDelete, | |||
| internal, | |||
| noWait, | |||
| args, | |||
| ) | |||
| } | |||
| // QueueBindSafe safely wraps the (*amqp.Channel).QueueBind method | |||
| func (connManager *ConnectionManager) QueueBindSafe( | |||
| name string, key string, exchange string, noWait bool, args amqp.Table, | |||
| ) error { | |||
| connManager.channelMux.RLock() | |||
| defer connManager.channelMux.RUnlock() | |||
| return connManager.channel.QueueBind( | |||
| name, | |||
| key, | |||
| exchange, | |||
| noWait, | |||
| args, | |||
| ) | |||
| } | |||
| // QosSafe safely wraps the (*amqp.Channel).Qos method | |||
| func (connManager *ConnectionManager) QosSafe( | |||
| prefetchCount int, prefetchSize int, global bool, | |||
| ) error { | |||
| connManager.channelMux.RLock() | |||
| defer connManager.channelMux.RUnlock() | |||
| return connManager.channel.Qos( | |||
| prefetchCount, | |||
| prefetchSize, | |||
| global, | |||
| ) | |||
| } | |||
| // PublishSafe safely wraps the (*amqp.Channel).Publish method | |||
| func (connManager *ConnectionManager) PublishSafe( | |||
| exchange string, key string, mandatory bool, immediate bool, msg amqp.Publishing, | |||
| ) error { | |||
| connManager.channelMux.RLock() | |||
| defer connManager.channelMux.RUnlock() | |||
| return connManager.channel.Publish( | |||
| exchange, | |||
| key, | |||
| mandatory, | |||
| immediate, | |||
| msg, | |||
| ) | |||
| } | |||
| // NotifyReturnSafe safely wraps the (*amqp.Channel).NotifyReturn method | |||
| func (connManager *ConnectionManager) NotifyReturnSafe( | |||
| c chan amqp.Return, | |||
| ) chan amqp.Return { | |||
| connManager.channelMux.RLock() | |||
| defer connManager.channelMux.RUnlock() | |||
| return connManager.channel.NotifyReturn( | |||
| c, | |||
| ) | |||
| } | |||
| // ConfirmSafe safely wraps the (*amqp.Channel).Confirm method | |||
| func (connManager *ConnectionManager) ConfirmSafe( | |||
| noWait bool, | |||
| ) error { | |||
| connManager.channelMux.RLock() | |||
| defer connManager.channelMux.RUnlock() | |||
| return connManager.channel.Confirm( | |||
| noWait, | |||
| ) | |||
| } | |||
| // NotifyPublishSafe safely wraps the (*amqp.Channel).NotifyPublish method | |||
| func (connManager *ConnectionManager) NotifyPublishSafe( | |||
| confirm chan amqp.Confirmation, | |||
| ) chan amqp.Confirmation { | |||
| connManager.channelMux.RLock() | |||
| defer connManager.channelMux.RUnlock() | |||
| return connManager.channel.NotifyPublish( | |||
| confirm, | |||
| ) | |||
| } | |||
| // NotifyFlowSafe safely wraps the (*amqp.Channel).NotifyFlow method | |||
| func (connManager *ConnectionManager) NotifyFlowSafe( | |||
| c chan bool, | |||
| ) chan bool { | |||
| connManager.channelMux.RLock() | |||
| defer connManager.channelMux.RUnlock() | |||
| return connManager.channel.NotifyFlow( | |||
| c, | |||
| ) | |||
| } | |||
| // NotifyBlockedSafe safely wraps the (*amqp.Connection).NotifyBlocked method | |||
| func (connManager *ConnectionManager) NotifyBlockedSafe( | |||
| receiver chan amqp.Blocking, | |||
| ) chan amqp.Blocking { | |||
| connManager.channelMux.RLock() | |||
| defer connManager.channelMux.RUnlock() | |||
| return connManager.connection.NotifyBlocked( | |||
| receiver, | |||
| ) | |||
| } | |||
| @ -0,0 +1,12 @@ | |||
| package logger | |||
| // Logger is describes a logging structure. It can be set using | |||
| // WithPublisherOptionsLogger() or WithConsumerOptionsLogger(). | |||
| type Logger interface { | |||
| Fatalf(string, ...interface{}) | |||
| Errorf(string, ...interface{}) | |||
| Warnf(string, ...interface{}) | |||
| Infof(string, ...interface{}) | |||
| Debugf(string, ...interface{}) | |||
| Tracef(string, ...interface{}) | |||
| } | |||