| @ -1,238 +1,245 @@ | |||
| package rabbitmq | |||
| import ( | |||
| "fmt" | |||
| "time" | |||
| "github.com/streadway/amqp" | |||
| ) | |||
| // Consumer allows you to create and connect to queues for data consumption. | |||
| type Consumer struct { | |||
| chManager *channelManager | |||
| logger Logger | |||
| } | |||
| // ConsumerOptions are used to describe a consumer's configuration. | |||
| // Logging set to true will enable the consumer to print to stdout | |||
| // Logger specifies a custom Logger interface implementation overruling Logging. | |||
| type ConsumerOptions struct { | |||
| Logging bool | |||
| Logger Logger | |||
| } | |||
| // Delivery captures the fields for a previously delivered message resident in | |||
| // a queue to be delivered by the server to a consumer from Channel.Consume or | |||
| // Channel.Get. | |||
| type Delivery struct { | |||
| amqp.Delivery | |||
| } | |||
| // NewConsumer returns a new Consumer connected to the given rabbitmq server | |||
| func NewConsumer(url string, optionFuncs ...func(*ConsumerOptions)) (Consumer, error) { | |||
| options := &ConsumerOptions{} | |||
| for _, optionFunc := range optionFuncs { | |||
| optionFunc(options) | |||
| } | |||
| if options.Logger == nil { | |||
| options.Logger = &noLogger{} // default no logging | |||
| } | |||
| chManager, err := newChannelManager(url, options.Logger) | |||
| if err != nil { | |||
| return Consumer{}, err | |||
| } | |||
| consumer := Consumer{ | |||
| chManager: chManager, | |||
| logger: options.Logger, | |||
| } | |||
| return consumer, nil | |||
| } | |||
| // WithConsumerOptionsLogging sets a logger to log to stdout | |||
| func WithConsumerOptionsLogging(options *ConsumerOptions) { | |||
| options.Logging = true | |||
| options.Logger = &stdLogger{} | |||
| } | |||
| // WithConsumerOptionsLogger sets logging to a custom interface. | |||
| // Use WithConsumerOptionsLogging to just log to stdout. | |||
| func WithConsumerOptionsLogger(log Logger) func(options *ConsumerOptions) { | |||
| return func(options *ConsumerOptions) { | |||
| options.Logging = true | |||
| options.Logger = log | |||
| } | |||
| } | |||
| // StartConsuming starts n goroutines where n="ConsumeOptions.QosOptions.Concurrency". | |||
| // Each goroutine spawns a handler that consumes off of the qiven queue which binds to the routing key(s). | |||
| // The provided handler is called once for each message. If the provided queue doesn't exist, it | |||
| // will be created on the cluster | |||
| func (consumer Consumer) StartConsuming( | |||
| handler func(d Delivery) bool, | |||
| queue string, | |||
| routingKeys []string, | |||
| optionFuncs ...func(*ConsumeOptions), | |||
| ) error { | |||
| defaultOptions := getDefaultConsumeOptions() | |||
| options := &ConsumeOptions{} | |||
| for _, optionFunc := range optionFuncs { | |||
| optionFunc(options) | |||
| } | |||
| if options.Concurrency < 1 { | |||
| options.Concurrency = defaultOptions.Concurrency | |||
| } | |||
| err := consumer.startGoroutines( | |||
| handler, | |||
| queue, | |||
| routingKeys, | |||
| *options, | |||
| ) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| go func() { | |||
| for err := range consumer.chManager.notifyCancelOrClose { | |||
| consumer.logger.Printf("consume cancel/close handler triggered. err: %v", err) | |||
| consumer.startGoroutinesWithRetries( | |||
| handler, | |||
| queue, | |||
| routingKeys, | |||
| *options, | |||
| ) | |||
| } | |||
| }() | |||
| return nil | |||
| } | |||
| // startGoroutinesWithRetries attempts to start consuming on a channel | |||
| // with an exponential backoff | |||
| func (consumer Consumer) startGoroutinesWithRetries( | |||
| handler func(d Delivery) bool, | |||
| queue string, | |||
| routingKeys []string, | |||
| consumeOptions ConsumeOptions, | |||
| ) { | |||
| backoffTime := time.Second | |||
| for { | |||
| consumer.logger.Printf("waiting %s seconds to attempt to start consumer goroutines", backoffTime) | |||
| time.Sleep(backoffTime) | |||
| backoffTime *= 2 | |||
| err := consumer.startGoroutines( | |||
| handler, | |||
| queue, | |||
| routingKeys, | |||
| consumeOptions, | |||
| ) | |||
| if err != nil { | |||
| consumer.logger.Printf("couldn't start consumer goroutines. err: %v", err) | |||
| continue | |||
| } | |||
| break | |||
| } | |||
| } | |||
| // startGoroutines declares the queue if it doesn't exist, | |||
| // binds the queue to the routing key(s), and starts the goroutines | |||
| // that will consume from the queue | |||
| func (consumer Consumer) startGoroutines( | |||
| handler func(d Delivery) bool, | |||
| queue string, | |||
| routingKeys []string, | |||
| consumeOptions ConsumeOptions, | |||
| ) error { | |||
| consumer.chManager.channelMux.RLock() | |||
| defer consumer.chManager.channelMux.RUnlock() | |||
| _, err := consumer.chManager.channel.QueueDeclare( | |||
| queue, | |||
| consumeOptions.QueueDurable, | |||
| consumeOptions.QueueAutoDelete, | |||
| consumeOptions.QueueExclusive, | |||
| consumeOptions.QueueNoWait, | |||
| tableToAMQPTable(consumeOptions.QueueArgs), | |||
| ) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| if consumeOptions.BindingExchange != nil { | |||
| exchange := consumeOptions.BindingExchange | |||
| if exchange.Name == "" { | |||
| return fmt.Errorf("binding to exchange but name not specified") | |||
| } | |||
| err = consumer.chManager.channel.ExchangeDeclare( | |||
| exchange.Name, | |||
| exchange.Kind, | |||
| exchange.Durable, | |||
| exchange.AutoDelete, | |||
| exchange.Internal, | |||
| exchange.NoWait, | |||
| tableToAMQPTable(exchange.ExchangeArgs), | |||
| ) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| for _, routingKey := range routingKeys { | |||
| err = consumer.chManager.channel.QueueBind( | |||
| queue, | |||
| routingKey, | |||
| exchange.Name, | |||
| consumeOptions.BindingNoWait, | |||
| tableToAMQPTable(consumeOptions.BindingArgs), | |||
| ) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| } | |||
| } | |||
| err = consumer.chManager.channel.Qos( | |||
| consumeOptions.QOSPrefetch, | |||
| 0, | |||
| consumeOptions.QOSGlobal, | |||
| ) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| msgs, err := consumer.chManager.channel.Consume( | |||
| queue, | |||
| consumeOptions.ConsumerName, | |||
| consumeOptions.ConsumerAutoAck, | |||
| consumeOptions.ConsumerExclusive, | |||
| consumeOptions.ConsumerNoLocal, // no-local is not supported by RabbitMQ | |||
| consumeOptions.ConsumerNoWait, | |||
| tableToAMQPTable(consumeOptions.ConsumerArgs), | |||
| ) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| for i := 0; i < consumeOptions.Concurrency; i++ { | |||
| go func() { | |||
| for msg := range msgs { | |||
| if consumeOptions.ConsumerAutoAck { | |||
| handler(Delivery{msg}) | |||
| continue | |||
| } | |||
| if handler(Delivery{msg}) { | |||
| err := msg.Ack(false) | |||
| if err != nil { | |||
| consumer.logger.Printf("can't ack message: %v", err) | |||
| } | |||
| } else { | |||
| err := msg.Nack(false, true) | |||
| if err != nil { | |||
| consumer.logger.Printf("can't nack message: %v", err) | |||
| } | |||
| } | |||
| } | |||
| consumer.logger.Printf("rabbit consumer goroutine closed") | |||
| }() | |||
| } | |||
| consumer.logger.Printf("Processing messages on %v goroutines", consumeOptions.Concurrency) | |||
| return nil | |||
| } | |||
| package rabbitmq | |||
| import ( | |||
| "fmt" | |||
| "time" | |||
| "github.com/streadway/amqp" | |||
| ) | |||
| // Consumer allows you to create and connect to queues for data consumption. | |||
| type Consumer struct { | |||
| chManager *channelManager | |||
| logger Logger | |||
| } | |||
| // ConsumerOptions are used to describe a consumer's configuration. | |||
| // Logging set to true will enable the consumer to print to stdout | |||
| // Logger specifies a custom Logger interface implementation overruling Logging. | |||
| type ConsumerOptions struct { | |||
| Logging bool | |||
| Logger Logger | |||
| } | |||
| // Delivery captures the fields for a previously delivered message resident in | |||
| // a queue to be delivered by the server to a consumer from Channel.Consume or | |||
| // Channel.Get. | |||
| type Delivery struct { | |||
| amqp.Delivery | |||
| } | |||
| // NewConsumer returns a new Consumer connected to the given rabbitmq server | |||
| func NewConsumer(url string, optionFuncs ...func(*ConsumerOptions)) (Consumer, error) { | |||
| options := &ConsumerOptions{} | |||
| for _, optionFunc := range optionFuncs { | |||
| optionFunc(options) | |||
| } | |||
| if options.Logger == nil { | |||
| options.Logger = &noLogger{} // default no logging | |||
| } | |||
| chManager, err := newChannelManager(url, options.Logger) | |||
| if err != nil { | |||
| return Consumer{}, err | |||
| } | |||
| consumer := Consumer{ | |||
| chManager: chManager, | |||
| logger: options.Logger, | |||
| } | |||
| return consumer, nil | |||
| } | |||
| // WithConsumerOptionsLogging sets a logger to log to stdout | |||
| func WithConsumerOptionsLogging(options *ConsumerOptions) { | |||
| options.Logging = true | |||
| options.Logger = &stdLogger{} | |||
| } | |||
| // WithConsumerOptionsLogger sets logging to a custom interface. | |||
| // Use WithConsumerOptionsLogging to just log to stdout. | |||
| func WithConsumerOptionsLogger(log Logger) func(options *ConsumerOptions) { | |||
| return func(options *ConsumerOptions) { | |||
| options.Logging = true | |||
| options.Logger = log | |||
| } | |||
| } | |||
| // StartConsuming starts n goroutines where n="ConsumeOptions.QosOptions.Concurrency". | |||
| // Each goroutine spawns a handler that consumes off of the qiven queue which binds to the routing key(s). | |||
| // The provided handler is called once for each message. If the provided queue doesn't exist, it | |||
| // will be created on the cluster | |||
| func (consumer Consumer) StartConsuming( | |||
| handler func(d Delivery) bool, | |||
| queue string, | |||
| routingKeys []string, | |||
| optionFuncs ...func(*ConsumeOptions), | |||
| ) error { | |||
| defaultOptions := getDefaultConsumeOptions() | |||
| options := &ConsumeOptions{} | |||
| for _, optionFunc := range optionFuncs { | |||
| optionFunc(options) | |||
| } | |||
| if options.Concurrency < 1 { | |||
| options.Concurrency = defaultOptions.Concurrency | |||
| } | |||
| err := consumer.startGoroutines( | |||
| handler, | |||
| queue, | |||
| routingKeys, | |||
| *options, | |||
| ) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| go func() { | |||
| for err := range consumer.chManager.notifyCancelOrClose { | |||
| consumer.logger.Printf("consume cancel/close handler triggered. err: %v", err) | |||
| consumer.startGoroutinesWithRetries( | |||
| handler, | |||
| queue, | |||
| routingKeys, | |||
| *options, | |||
| ) | |||
| } | |||
| }() | |||
| return nil | |||
| } | |||
| // StopConsuming stops the consumption of messages. | |||
| // The consumer should be discarded as it's not safe for re-use | |||
| func (consumer Consumer) StopConsuming() { | |||
| consumer.chManager.channel.Close() | |||
| consumer.chManager.connection.Close() | |||
| } | |||
| // startGoroutinesWithRetries attempts to start consuming on a channel | |||
| // with an exponential backoff | |||
| func (consumer Consumer) startGoroutinesWithRetries( | |||
| handler func(d Delivery) bool, | |||
| queue string, | |||
| routingKeys []string, | |||
| consumeOptions ConsumeOptions, | |||
| ) { | |||
| backoffTime := time.Second | |||
| for { | |||
| consumer.logger.Printf("waiting %s seconds to attempt to start consumer goroutines", backoffTime) | |||
| time.Sleep(backoffTime) | |||
| backoffTime *= 2 | |||
| err := consumer.startGoroutines( | |||
| handler, | |||
| queue, | |||
| routingKeys, | |||
| consumeOptions, | |||
| ) | |||
| if err != nil { | |||
| consumer.logger.Printf("couldn't start consumer goroutines. err: %v", err) | |||
| continue | |||
| } | |||
| break | |||
| } | |||
| } | |||
| // startGoroutines declares the queue if it doesn't exist, | |||
| // binds the queue to the routing key(s), and starts the goroutines | |||
| // that will consume from the queue | |||
| func (consumer Consumer) startGoroutines( | |||
| handler func(d Delivery) bool, | |||
| queue string, | |||
| routingKeys []string, | |||
| consumeOptions ConsumeOptions, | |||
| ) error { | |||
| consumer.chManager.channelMux.RLock() | |||
| defer consumer.chManager.channelMux.RUnlock() | |||
| _, err := consumer.chManager.channel.QueueDeclare( | |||
| queue, | |||
| consumeOptions.QueueDurable, | |||
| consumeOptions.QueueAutoDelete, | |||
| consumeOptions.QueueExclusive, | |||
| consumeOptions.QueueNoWait, | |||
| tableToAMQPTable(consumeOptions.QueueArgs), | |||
| ) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| if consumeOptions.BindingExchange != nil { | |||
| exchange := consumeOptions.BindingExchange | |||
| if exchange.Name == "" { | |||
| return fmt.Errorf("binding to exchange but name not specified") | |||
| } | |||
| err = consumer.chManager.channel.ExchangeDeclare( | |||
| exchange.Name, | |||
| exchange.Kind, | |||
| exchange.Durable, | |||
| exchange.AutoDelete, | |||
| exchange.Internal, | |||
| exchange.NoWait, | |||
| tableToAMQPTable(exchange.ExchangeArgs), | |||
| ) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| for _, routingKey := range routingKeys { | |||
| err = consumer.chManager.channel.QueueBind( | |||
| queue, | |||
| routingKey, | |||
| exchange.Name, | |||
| consumeOptions.BindingNoWait, | |||
| tableToAMQPTable(consumeOptions.BindingArgs), | |||
| ) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| } | |||
| } | |||
| err = consumer.chManager.channel.Qos( | |||
| consumeOptions.QOSPrefetch, | |||
| 0, | |||
| consumeOptions.QOSGlobal, | |||
| ) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| msgs, err := consumer.chManager.channel.Consume( | |||
| queue, | |||
| consumeOptions.ConsumerName, | |||
| consumeOptions.ConsumerAutoAck, | |||
| consumeOptions.ConsumerExclusive, | |||
| consumeOptions.ConsumerNoLocal, // no-local is not supported by RabbitMQ | |||
| consumeOptions.ConsumerNoWait, | |||
| tableToAMQPTable(consumeOptions.ConsumerArgs), | |||
| ) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| for i := 0; i < consumeOptions.Concurrency; i++ { | |||
| go func() { | |||
| for msg := range msgs { | |||
| if consumeOptions.ConsumerAutoAck { | |||
| handler(Delivery{msg}) | |||
| continue | |||
| } | |||
| if handler(Delivery{msg}) { | |||
| err := msg.Ack(false) | |||
| if err != nil { | |||
| consumer.logger.Printf("can't ack message: %v", err) | |||
| } | |||
| } else { | |||
| err := msg.Nack(false, true) | |||
| if err != nil { | |||
| consumer.logger.Printf("can't nack message: %v", err) | |||
| } | |||
| } | |||
| } | |||
| consumer.logger.Printf("rabbit consumer goroutine closed") | |||
| }() | |||
| } | |||
| consumer.logger.Printf("Processing messages on %v goroutines", consumeOptions.Concurrency) | |||
| return nil | |||
| } | |||