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 }