github.com/moleculer-go/moleculer@v0.3.3/transit/amqp/amqp.go (about)

     1  package amqp
     2  
     3  import (
     4  	"fmt"
     5  	"github.com/moleculer-go/moleculer"
     6  	"github.com/moleculer-go/moleculer/serializer"
     7  	"github.com/moleculer-go/moleculer/transit"
     8  	"github.com/pkg/errors"
     9  	log "github.com/sirupsen/logrus"
    10  	"github.com/streadway/amqp"
    11  	"strings"
    12  	"time"
    13  )
    14  
    15  const (
    16  	DurationNotDefined = time.Duration(-1)
    17  )
    18  
    19  type safeHandler func(moleculer.Payload) error
    20  
    21  type binding struct {
    22  	queueName string
    23  	topic     string
    24  	pattern   string
    25  }
    26  
    27  type subscriber struct {
    28  	command string
    29  	nodeID  string
    30  	handler transit.TransportHandler
    31  }
    32  
    33  var DefaultConfig = AmqpOptions{
    34  	Prefetch: 1,
    35  
    36  	AutoDeleteQueues:    DurationNotDefined,
    37  	EventTimeToLive:     DurationNotDefined,
    38  	HeartbeatTimeToLive: DurationNotDefined,
    39  }
    40  
    41  type AmqpTransporter struct {
    42  	opts       *AmqpOptions
    43  	prefix     string
    44  	logger     *log.Entry
    45  	serializer serializer.Serializer
    46  
    47  	connectionDisconnecting bool
    48  	connectionRecovering    bool
    49  	connection              *amqp.Connection
    50  	channel                 *amqp.Channel
    51  
    52  	nodeID      string
    53  	subscribers []subscriber
    54  	bindings    []binding
    55  }
    56  
    57  type AmqpOptions struct {
    58  	Url             []string
    59  	QueueOptions    map[string]interface{}
    60  	ExchangeOptions map[string]interface{}
    61  	MessageOptions  map[string]interface{}
    62  	ConsumeOptions  amqp.Table
    63  
    64  	Logger     *log.Entry
    65  	Serializer serializer.Serializer
    66  
    67  	DisableReconnect    bool
    68  	AutoDeleteQueues    time.Duration
    69  	EventTimeToLive     time.Duration
    70  	HeartbeatTimeToLive time.Duration
    71  	Prefetch            int
    72  }
    73  
    74  func mergeConfigs(baseConfig AmqpOptions, userConfig AmqpOptions) AmqpOptions {
    75  	// Number of requests a broker will handle concurrently
    76  	if userConfig.Prefetch != 0 {
    77  		baseConfig.Prefetch = userConfig.Prefetch
    78  	}
    79  
    80  	// Number of milliseconds before an event expires
    81  	if userConfig.EventTimeToLive != 0 {
    82  		baseConfig.EventTimeToLive = userConfig.EventTimeToLive
    83  	}
    84  
    85  	if userConfig.HeartbeatTimeToLive != 0 {
    86  		baseConfig.HeartbeatTimeToLive = userConfig.HeartbeatTimeToLive
    87  	}
    88  
    89  	if userConfig.QueueOptions != nil {
    90  		baseConfig.QueueOptions = userConfig.QueueOptions
    91  	}
    92  
    93  	if userConfig.ExchangeOptions != nil {
    94  		baseConfig.ExchangeOptions = userConfig.ExchangeOptions
    95  	}
    96  
    97  	if userConfig.MessageOptions != nil {
    98  		baseConfig.MessageOptions = userConfig.MessageOptions
    99  	}
   100  
   101  	if userConfig.ConsumeOptions != nil {
   102  		baseConfig.ConsumeOptions = userConfig.ConsumeOptions
   103  	}
   104  
   105  	if userConfig.AutoDeleteQueues != 0 {
   106  		baseConfig.AutoDeleteQueues = userConfig.AutoDeleteQueues
   107  	}
   108  
   109  	baseConfig.DisableReconnect = userConfig.DisableReconnect
   110  
   111  	// Support for multiple URLs (clusters)
   112  	if len(userConfig.Url) != 0 {
   113  		baseConfig.Url = userConfig.Url
   114  	}
   115  
   116  	if userConfig.Logger != nil {
   117  		baseConfig.Logger = userConfig.Logger
   118  	}
   119  
   120  	return baseConfig
   121  }
   122  
   123  func CreateAmqpTransporter(options AmqpOptions) transit.Transport {
   124  	options = mergeConfigs(DefaultConfig, options)
   125  
   126  	return &AmqpTransporter{
   127  		opts:   &options,
   128  		logger: options.Logger,
   129  	}
   130  }
   131  
   132  func (t *AmqpTransporter) Connect() chan error {
   133  	endChan := make(chan error)
   134  
   135  	go func() {
   136  		t.logger.Debug("AMQP Connect() - url: ", t.opts.Url)
   137  
   138  		isConnected := false
   139  		connectAttempt := 0
   140  
   141  		for {
   142  			connectAttempt++
   143  			urlIndex := (connectAttempt - 1) % len(t.opts.Url)
   144  			uri := t.opts.Url[urlIndex]
   145  
   146  			closeNotifyChan, err := t.doConnect(uri)
   147  			if err != nil {
   148  				t.logger.Error("AMQP Connect() - Error: ", err, " url: ", uri)
   149  			} else if !isConnected {
   150  				isConnected = true
   151  				endChan <- nil
   152  			} else {
   153  				// recovery subscribers
   154  				for _, subscriber := range t.subscribers {
   155  					t.subscribeInternal(subscriber)
   156  				}
   157  
   158  				t.connectionRecovering = false
   159  			}
   160  
   161  			if closeNotifyChan != nil {
   162  				err = <-closeNotifyChan
   163  				if t.connectionDisconnecting {
   164  					t.logger.Info("AMQP connection is closed gracefully")
   165  					return
   166  				}
   167  
   168  				t.logger.Error("AMQP connection is closed -> ", err)
   169  			}
   170  
   171  			if t.opts.DisableReconnect {
   172  				return
   173  			}
   174  
   175  			t.connectionRecovering = true
   176  
   177  			time.Sleep(5 * time.Second)
   178  		}
   179  	}()
   180  	return endChan
   181  }
   182  
   183  func (t *AmqpTransporter) doConnect(uri string) (chan *amqp.Error, error) {
   184  	var err error
   185  
   186  	t.connection, err = amqp.Dial(uri)
   187  	if err != nil {
   188  		return nil, errors.Wrap(err, "AMQP failed to connect")
   189  	}
   190  
   191  	t.logger.Info("AMQP is connected")
   192  
   193  	if t.channel, err = t.connection.Channel(); err != nil {
   194  		return nil, errors.Wrap(err, "AMQP failed to create channel")
   195  	}
   196  
   197  	t.logger.Info("AMQP channel is created")
   198  
   199  	if err := t.channel.Qos(t.opts.Prefetch, 0, false); err != nil {
   200  		return nil, errors.Wrap(err, "AMQP failed set prefetch count")
   201  	}
   202  
   203  	closeNotifyChan := make(chan *amqp.Error)
   204  	t.connection.NotifyClose(closeNotifyChan)
   205  
   206  	return closeNotifyChan, nil
   207  }
   208  
   209  func (t *AmqpTransporter) Disconnect() chan error {
   210  	errChan := make(chan error)
   211  
   212  	t.connectionDisconnecting = true
   213  
   214  	go func() {
   215  		if t.connection != nil && t.channel != nil {
   216  			for _, bind := range t.bindings {
   217  				if err := t.channel.QueueUnbind(bind.queueName, bind.pattern, bind.topic, nil); err != nil {
   218  					t.logger.Errorf("AMQP Disconnect() - Can't unbind queue '%#v': %s", bind, err)
   219  				}
   220  			}
   221  
   222  			t.subscribers = []subscriber{}
   223  			t.bindings = []binding{}
   224  			t.connectionDisconnecting = true
   225  
   226  			if err := t.channel.Close(); err != nil {
   227  				t.logger.Error("AMQP Disconnect() - Channel close error: ", err)
   228  				errChan <- err
   229  				return
   230  			}
   231  
   232  			t.channel = nil
   233  
   234  			if err := t.connection.Close(); err != nil {
   235  				t.logger.Error("AMQP Disconnect() - Connection close error: ", err)
   236  				errChan <- err
   237  				return
   238  			}
   239  
   240  			t.connection = nil
   241  		}
   242  
   243  		errChan <- nil
   244  	}()
   245  
   246  	return errChan
   247  }
   248  
   249  func (t *AmqpTransporter) Subscribe(command, nodeID string, handler transit.TransportHandler) {
   250  	subscriber := subscriber{command, nodeID, handler}
   251  
   252  	// Save subscribers for recovery logic
   253  	t.subscribers = append(t.subscribers, subscriber)
   254  
   255  	t.subscribeInternal(subscriber)
   256  }
   257  
   258  func (t *AmqpTransporter) subscribeInternal(subscriber subscriber) {
   259  	if t.channel == nil {
   260  		return
   261  	}
   262  
   263  	topic := t.topicName(subscriber.command, subscriber.nodeID)
   264  
   265  	if subscriber.nodeID != "" {
   266  		// Some topics are specific to this node already, in these cases we don't need an exchange.
   267  		needAck := subscriber.command == "REQ"
   268  		autoDelete, durable, exclusive, args := t.getQueueOptions(subscriber.command, false)
   269  		if _, err := t.channel.QueueDeclare(topic, durable, autoDelete, exclusive, false, args); err != nil {
   270  			t.logger.Error("AMQP Subscribe() - Queue declare error: ", err)
   271  			return
   272  		}
   273  
   274  		go t.doConsume(topic, needAck, subscriber.handler)
   275  	} else {
   276  		// Create a queue specific to this nodeID so that this node can receive broadcasted messages.
   277  		queueName := t.prefix + "." + subscriber.command + "." + t.nodeID
   278  
   279  		// Save binding arguments for easy unbinding later.
   280  		b := binding{
   281  			queueName: queueName,
   282  			topic:     topic,
   283  			pattern:   "",
   284  		}
   285  		t.bindings = append(t.bindings, b)
   286  
   287  		autoDelete, durable, exclusive, args := t.getQueueOptions(subscriber.command, false)
   288  		if _, err := t.channel.QueueDeclare(queueName, durable, autoDelete, exclusive, false, args); err != nil {
   289  			t.logger.Error("AMQP Subscribe() - Queue declare error: ", err)
   290  			return
   291  		}
   292  
   293  		durable, autoDelete, args = t.getExchangeOptions()
   294  		if err := t.channel.ExchangeDeclare(topic, "fanout", durable, autoDelete, false, false, args); err != nil {
   295  			t.logger.Error("AMQP Subscribe() - Exchange declare error: ", err)
   296  			return
   297  		}
   298  
   299  		if err := t.channel.QueueBind(b.queueName, b.pattern, b.topic, false, nil); err != nil {
   300  			t.logger.Error("AMQP Subscribe() - Can't bind queue to exchange: ", err)
   301  			return
   302  		}
   303  
   304  		go t.doConsume(queueName, false, subscriber.handler)
   305  	}
   306  }
   307  
   308  func (t *AmqpTransporter) Publish(command, nodeID string, message moleculer.Payload) {
   309  	if t.channel == nil {
   310  		msg := fmt.Sprint("AMQP Publish() No connection -> command: ", command, " nodeID: ", nodeID)
   311  		t.logger.Error(msg)
   312  		panic(errors.New(msg))
   313  	}
   314  
   315  	if t.connectionRecovering {
   316  		t.waitForRecovering()
   317  	}
   318  
   319  	topic := t.topicName(command, nodeID)
   320  	routingKey := ""
   321  
   322  	if nodeID != "" {
   323  		routingKey = topic
   324  		topic = ""
   325  	}
   326  
   327  	data := t.serializer.PayloadToBytes(message)
   328  
   329  	msg := amqp.Publishing{
   330  		Body: data,
   331  	}
   332  
   333  	if err := t.channel.Publish(topic, routingKey, false, false, msg); err != nil {
   334  		t.logger.Warnf("AMQP Publish - Can't publish command: %s, nodeID: %s, error: %s", command, nodeID, err)
   335  	}
   336  }
   337  
   338  func (t *AmqpTransporter) waitForRecovering() {
   339  	for {
   340  		if !t.connectionRecovering {
   341  			return
   342  		}
   343  
   344  		time.Sleep(time.Second)
   345  	}
   346  }
   347  
   348  func (t *AmqpTransporter) SetPrefix(prefix string) {
   349  	t.prefix = prefix
   350  }
   351  
   352  func (t *AmqpTransporter) SetNodeID(nodeID string) {
   353  	t.nodeID = nodeID
   354  }
   355  
   356  func (t *AmqpTransporter) SetSerializer(serializer serializer.Serializer) {
   357  	t.serializer = serializer
   358  }
   359  
   360  func (t *AmqpTransporter) doConsume(queueName string, needAck bool, handler transit.TransportHandler) {
   361  	t.logger.Debug("AMQP doConsume() - queue: ", queueName)
   362  
   363  	msgs, err := t.channel.Consume(queueName, "", !needAck, false, false, true, t.opts.ConsumeOptions)
   364  	if err != nil {
   365  		t.logger.Errorf("AMQP doConsume - Can't start consume for queue '%s': %s", queueName, err)
   366  		return
   367  	}
   368  
   369  	for {
   370  		msg, ok := <-msgs
   371  		if !ok {
   372  			break
   373  		}
   374  
   375  		payload := t.serializer.BytesToPayload(&msg.Body)
   376  		t.logger.Debugf("Incoming %s packet from '%s'", queueName, payload.Get("sender").String())
   377  
   378  		handler(payload)
   379  
   380  		if needAck {
   381  			if err = msg.Ack(false); err != nil {
   382  				t.logger.Error("AMQP doConsume() - Can't acknowledge message: ", err)
   383  
   384  				if err = msg.Nack(false, true); err != nil {
   385  					t.logger.Error("AMQP doConsume() - Can't negatively acknowledge message: ", err)
   386  				}
   387  			}
   388  		}
   389  	}
   390  }
   391  
   392  func (t *AmqpTransporter) getExchangeOptions() (durable, autoDelete bool, args amqp.Table) {
   393  	args = amqp.Table{}
   394  
   395  	for key, value := range t.opts.ExchangeOptions {
   396  		switch key {
   397  		case "durable":
   398  			durable = value.(bool)
   399  		case "autoDelete":
   400  			autoDelete = value.(bool)
   401  		default:
   402  			args[key] = value
   403  		}
   404  	}
   405  
   406  	return durable, autoDelete, args
   407  }
   408  
   409  func (t *AmqpTransporter) getQueueOptions(command string, balancedQueue bool) (autoDelete, durable, exclusive bool, args amqp.Table) {
   410  	args = amqp.Table{}
   411  
   412  	switch command {
   413  	// Requests and responses don't expire.
   414  	case "REQ":
   415  		if t.opts.AutoDeleteQueues != DurationNotDefined && !balancedQueue {
   416  			args["x-expires"] = int(t.opts.AutoDeleteQueues / time.Millisecond)
   417  		}
   418  
   419  	case "RES":
   420  		if t.opts.AutoDeleteQueues != DurationNotDefined {
   421  			args["x-expires"] = int(t.opts.AutoDeleteQueues / time.Millisecond)
   422  		}
   423  
   424  	// Consumers can decide how long events live
   425  	// Load-balanced/grouped events
   426  	case "EVENT", "EVENTLB":
   427  		if t.opts.AutoDeleteQueues != DurationNotDefined {
   428  			args["x-expires"] = int(t.opts.AutoDeleteQueues / time.Millisecond)
   429  		}
   430  
   431  		// If eventTimeToLive is specified, add to options.
   432  		if t.opts.EventTimeToLive != DurationNotDefined {
   433  			args["x-message-ttl"] = int(t.opts.AutoDeleteQueues / time.Millisecond)
   434  		}
   435  
   436  	// Packet types meant for internal use
   437  	case "HEARTBEAT":
   438  		autoDelete = true
   439  
   440  		// If heartbeatTimeToLive is specified, add to options.
   441  		if t.opts.HeartbeatTimeToLive != DurationNotDefined {
   442  			args["x-message-ttl"] = int(t.opts.AutoDeleteQueues / time.Millisecond)
   443  		}
   444  	case "DISCOVER", "DISCONNECT", "INFO", "PING", "PONG":
   445  		autoDelete = true
   446  	}
   447  
   448  	for key, value := range t.opts.QueueOptions {
   449  		switch key {
   450  		case "exclusive":
   451  			exclusive = value.(bool)
   452  		case "durable":
   453  			durable = value.(bool)
   454  		default:
   455  			args[key] = value
   456  		}
   457  	}
   458  
   459  	return autoDelete, durable, exclusive, args
   460  }
   461  
   462  func (t *AmqpTransporter) topicName(command string, nodeID string) string {
   463  	parts := []string{t.prefix, command}
   464  	if nodeID != "" {
   465  		parts = append(parts, nodeID)
   466  	}
   467  	return strings.Join(parts, ".")
   468  }