go-micro.dev/v5@v5.12.0/broker/rabbitmq/rabbitmq.go (about) 1 // Package rabbitmq provides a RabbitMQ broker 2 package rabbitmq 3 4 import ( 5 "context" 6 "errors" 7 "fmt" 8 "net/url" 9 "sync" 10 "time" 11 12 amqp "github.com/rabbitmq/amqp091-go" 13 "go-micro.dev/v5/broker" 14 "go-micro.dev/v5/logger" 15 ) 16 17 type rbroker struct { 18 conn *rabbitMQConn 19 addrs []string 20 opts broker.Options 21 prefetchCount int 22 prefetchGlobal bool 23 mtx sync.Mutex 24 wg sync.WaitGroup 25 } 26 27 type subscriber struct { 28 mtx sync.Mutex 29 unsub chan bool 30 opts broker.SubscribeOptions 31 topic string 32 ch *rabbitMQChannel 33 durableQueue bool 34 queueArgs map[string]interface{} 35 r *rbroker 36 fn func(msg amqp.Delivery) 37 headers map[string]interface{} 38 wg sync.WaitGroup 39 } 40 41 type publication struct { 42 d amqp.Delivery 43 m *broker.Message 44 t string 45 err error 46 } 47 48 func (p *publication) Ack() error { 49 return p.d.Ack(false) 50 } 51 52 func (p *publication) Error() error { 53 return p.err 54 } 55 56 func (p *publication) Topic() string { 57 return p.t 58 } 59 60 func (p *publication) Message() *broker.Message { 61 return p.m 62 } 63 64 func (s *subscriber) Options() broker.SubscribeOptions { 65 return s.opts 66 } 67 68 func (s *subscriber) Topic() string { 69 return s.topic 70 } 71 72 func (s *subscriber) Unsubscribe() error { 73 s.unsub <- true 74 75 // Need to wait on subscriber to exit if autoack is disabled 76 // since closing the channel will prevent the ack/nack from 77 // being sent upon handler completion. 78 if !s.opts.AutoAck { 79 s.wg.Wait() 80 } 81 82 s.mtx.Lock() 83 defer s.mtx.Unlock() 84 if s.ch != nil { 85 return s.ch.Close() 86 } 87 return nil 88 } 89 90 func (s *subscriber) resubscribe() { 91 s.wg.Add(1) 92 defer s.wg.Done() 93 94 minResubscribeDelay := 100 * time.Millisecond 95 maxResubscribeDelay := 30 * time.Second 96 expFactor := time.Duration(2) 97 reSubscribeDelay := minResubscribeDelay 98 // loop until unsubscribe 99 for { 100 select { 101 // unsubscribe case 102 case <-s.unsub: 103 return 104 // check shutdown case 105 case <-s.r.conn.close: 106 // yep, its shutdown case 107 return 108 // wait until we reconect to rabbit 109 case <-s.r.conn.waitConnection: 110 // When the connection is disconnected, the waitConnection will be re-assigned, so '<-s.r.conn.waitConnection' maybe blocked. 111 // Here, it returns once a second, and then the latest waitconnection will be used 112 case <-time.After(time.Second): 113 continue 114 } 115 116 // it may crash (panic) in case of Consume without connection, so recheck it 117 s.r.mtx.Lock() 118 if !s.r.conn.connected { 119 s.r.mtx.Unlock() 120 continue 121 } 122 123 ch, sub, err := s.r.conn.Consume( 124 s.opts.Queue, 125 s.topic, 126 s.headers, 127 s.queueArgs, 128 s.opts.AutoAck, 129 s.durableQueue, 130 ) 131 132 s.r.mtx.Unlock() 133 switch err { 134 case nil: 135 reSubscribeDelay = minResubscribeDelay 136 s.mtx.Lock() 137 s.ch = ch 138 s.mtx.Unlock() 139 default: 140 if reSubscribeDelay > maxResubscribeDelay { 141 reSubscribeDelay = maxResubscribeDelay 142 } 143 time.Sleep(reSubscribeDelay) 144 reSubscribeDelay *= expFactor 145 continue 146 } 147 148 SubLoop: 149 for { 150 select { 151 case <-s.unsub: 152 return 153 case d, ok := <-sub: 154 if !ok { 155 break SubLoop 156 } 157 s.r.wg.Add(1) 158 s.fn(d) 159 s.r.wg.Done() 160 } 161 } 162 } 163 } 164 165 func (r *rbroker) Publish(topic string, msg *broker.Message, opts ...broker.PublishOption) error { 166 m := amqp.Publishing{ 167 Body: msg.Body, 168 Headers: amqp.Table{}, 169 } 170 171 options := broker.PublishOptions{} 172 for _, o := range opts { 173 o(&options) 174 } 175 176 if options.Context != nil { 177 if value, ok := options.Context.Value(deliveryMode{}).(uint8); ok { 178 m.DeliveryMode = value 179 } 180 181 if value, ok := options.Context.Value(priorityKey{}).(uint8); ok { 182 m.Priority = value 183 } 184 185 if value, ok := options.Context.Value(contentType{}).(string); ok { 186 m.Headers["Content-Type"] = value 187 m.ContentType = value 188 } 189 190 if value, ok := options.Context.Value(contentEncoding{}).(string); ok { 191 m.ContentEncoding = value 192 } 193 194 if value, ok := options.Context.Value(correlationID{}).(string); ok { 195 m.CorrelationId = value 196 } 197 198 if value, ok := options.Context.Value(replyTo{}).(string); ok { 199 m.ReplyTo = value 200 } 201 202 if value, ok := options.Context.Value(expiration{}).(string); ok { 203 m.Expiration = value 204 } 205 206 if value, ok := options.Context.Value(messageID{}).(string); ok { 207 m.MessageId = value 208 } 209 210 if value, ok := options.Context.Value(timestamp{}).(time.Time); ok { 211 m.Timestamp = value 212 } 213 214 if value, ok := options.Context.Value(typeMsg{}).(string); ok { 215 m.Type = value 216 } 217 218 if value, ok := options.Context.Value(userID{}).(string); ok { 219 m.UserId = value 220 } 221 222 if value, ok := options.Context.Value(appID{}).(string); ok { 223 m.AppId = value 224 } 225 } 226 227 for k, v := range msg.Header { 228 m.Headers[k] = v 229 } 230 231 if r.getWithoutExchange() { 232 m.Headers["Micro-Topic"] = topic 233 } 234 235 if r.conn == nil { 236 return errors.New("connection is nil") 237 } 238 239 return r.conn.Publish(r.conn.exchange.Name, topic, m) 240 } 241 242 func (r *rbroker) Subscribe(topic string, handler broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) { 243 var ackSuccess bool 244 245 if r.conn == nil { 246 return nil, errors.New("not connected") 247 } 248 249 opt := broker.SubscribeOptions{ 250 AutoAck: true, 251 } 252 253 for _, o := range opts { 254 o(&opt) 255 } 256 257 // Make sure context is setup 258 if opt.Context == nil { 259 opt.Context = context.Background() 260 } 261 262 ctx := opt.Context 263 if subscribeContext, ok := ctx.Value(subscribeContextKey{}).(context.Context); ok && subscribeContext != nil { 264 ctx = subscribeContext 265 } 266 267 var requeueOnError bool 268 requeueOnError, _ = ctx.Value(requeueOnErrorKey{}).(bool) 269 270 var durableQueue bool 271 durableQueue, _ = ctx.Value(durableQueueKey{}).(bool) 272 273 var qArgs map[string]interface{} 274 if qa, ok := ctx.Value(queueArgumentsKey{}).(map[string]interface{}); ok { 275 qArgs = qa 276 } 277 278 var headers map[string]interface{} 279 if h, ok := ctx.Value(headersKey{}).(map[string]interface{}); ok { 280 headers = h 281 } 282 283 if bval, ok := ctx.Value(ackSuccessKey{}).(bool); ok && bval { 284 opt.AutoAck = false 285 ackSuccess = true 286 } 287 288 fn := func(msg amqp.Delivery) { 289 header := make(map[string]string) 290 for k, v := range msg.Headers { 291 header[k] = fmt.Sprintf("%v", v) 292 } 293 294 // Get rid of dependence on 'Micro-Topic' 295 msgTopic := header["Micro-Topic"] 296 if msgTopic == "" { 297 header["Micro-Topic"] = msg.RoutingKey 298 } 299 300 m := &broker.Message{ 301 Header: header, 302 Body: msg.Body, 303 } 304 p := &publication{d: msg, m: m, t: msg.RoutingKey} 305 p.err = handler(p) 306 if p.err == nil && ackSuccess && !opt.AutoAck { 307 msg.Ack(false) 308 } else if p.err != nil && !opt.AutoAck { 309 msg.Nack(false, requeueOnError) 310 } 311 } 312 313 sret := &subscriber{topic: topic, opts: opt, unsub: make(chan bool), r: r, 314 durableQueue: durableQueue, fn: fn, headers: headers, queueArgs: qArgs, 315 wg: sync.WaitGroup{}} 316 317 go sret.resubscribe() 318 319 return sret, nil 320 } 321 322 func (r *rbroker) Options() broker.Options { 323 return r.opts 324 } 325 326 func (r *rbroker) String() string { 327 return "rabbitmq" 328 } 329 330 func (r *rbroker) Address() string { 331 if len(r.addrs) > 0 { 332 u, err := url.Parse(r.addrs[0]) 333 if err != nil { 334 return "" 335 } 336 337 return u.Redacted() 338 } 339 return "" 340 } 341 342 func (r *rbroker) Init(opts ...broker.Option) error { 343 for _, o := range opts { 344 o(&r.opts) 345 } 346 r.addrs = r.opts.Addrs 347 return nil 348 } 349 350 func (r *rbroker) Connect() error { 351 if r.conn == nil { 352 r.conn = newRabbitMQConn( 353 r.getExchange(), 354 r.opts.Addrs, 355 r.getPrefetchCount(), 356 r.getPrefetchGlobal(), 357 r.getConfirmPublish(), 358 r.getWithoutExchange(), 359 r.opts.Logger, 360 ) 361 } 362 363 conf := defaultAmqpConfig 364 365 if auth, ok := r.opts.Context.Value(externalAuth{}).(ExternalAuthentication); ok { 366 conf.SASL = []amqp.Authentication{&auth} 367 } 368 369 conf.TLSClientConfig = r.opts.TLSConfig 370 371 return r.conn.Connect(r.opts.Secure, &conf) 372 } 373 374 func (r *rbroker) Disconnect() error { 375 if r.conn == nil { 376 return errors.New("connection is nil") 377 } 378 ret := r.conn.Close() 379 r.wg.Wait() // wait all goroutines 380 return ret 381 } 382 383 func NewBroker(opts ...broker.Option) broker.Broker { 384 options := broker.Options{ 385 Context: context.Background(), 386 Logger: logger.DefaultLogger, 387 } 388 389 for _, o := range opts { 390 o(&options) 391 } 392 393 return &rbroker{ 394 addrs: options.Addrs, 395 opts: options, 396 } 397 } 398 399 func (r *rbroker) getExchange() Exchange { 400 ex := DefaultExchange 401 402 if e, ok := r.opts.Context.Value(exchangeKey{}).(string); ok { 403 ex.Name = e 404 } 405 406 if t, ok := r.opts.Context.Value(exchangeTypeKey{}).(MQExchangeType); ok { 407 ex.Type = t 408 } 409 410 if d, ok := r.opts.Context.Value(durableExchange{}).(bool); ok { 411 ex.Durable = d 412 } 413 414 return ex 415 } 416 417 func (r *rbroker) getPrefetchCount() int { 418 if e, ok := r.opts.Context.Value(prefetchCountKey{}).(int); ok { 419 return e 420 } 421 return DefaultPrefetchCount 422 } 423 424 func (r *rbroker) getPrefetchGlobal() bool { 425 if e, ok := r.opts.Context.Value(prefetchGlobalKey{}).(bool); ok { 426 return e 427 } 428 return DefaultPrefetchGlobal 429 } 430 431 func (r *rbroker) getConfirmPublish() bool { 432 if e, ok := r.opts.Context.Value(confirmPublishKey{}).(bool); ok { 433 return e 434 } 435 return DefaultConfirmPublish 436 } 437 438 func (r *rbroker) getWithoutExchange() bool { 439 if e, ok := r.opts.Context.Value(withoutExchangeKey{}).(bool); ok { 440 return e 441 } 442 return DefaultWithoutExchange 443 }