github.com/wfusion/gofusion@v1.1.14/common/infra/watermill/pubsub/amqp/subscriber.go (about) 1 package amqp 2 3 import ( 4 "context" 5 "sync" 6 "time" 7 8 "github.com/pkg/errors" 9 "go.uber.org/atomic" 10 "go.uber.org/multierr" 11 12 "github.com/wfusion/gofusion/common/infra/watermill" 13 "github.com/wfusion/gofusion/common/infra/watermill/message" 14 15 amqp "github.com/rabbitmq/amqp091-go" 16 ) 17 18 type Subscriber struct { 19 *ConnectionWrapper 20 21 config Config 22 closedChan chan struct{} 23 closeSubscriber func() error 24 subscriberWaitGroup *sync.WaitGroup 25 } 26 27 func NewSubscriber(config Config, logger watermill.LoggerAdapter) (*Subscriber, error) { 28 if err := config.ValidateSubscriber(); err != nil { 29 return nil, err 30 } 31 32 conn, err := NewConnection(config.Connection, logger) 33 if err != nil { 34 return nil, err 35 } 36 37 var closed atomic.Uint32 38 closedChan := make(chan struct{}) 39 var subscriberWaitGroup sync.WaitGroup 40 41 // Close the subscriber AND the connection when the subscriber is closed, 42 // since this subscriber owns the connection. 43 closeSubscriber := func() error { 44 if !closed.CompareAndSwap(0, 1) { 45 // Already closed. 46 return nil 47 } 48 49 logger.Debug("[Common] watermill amqp closing subscriber", nil) 50 51 close(closedChan) 52 53 subscriberWaitGroup.Wait() 54 55 logger.Debug("[Common] watermill amqp closing connection", nil) 56 57 return conn.Close() 58 } 59 60 return &Subscriber{ 61 ConnectionWrapper: conn, 62 config: config, 63 closedChan: closedChan, 64 closeSubscriber: closeSubscriber, 65 subscriberWaitGroup: &subscriberWaitGroup, 66 }, nil 67 } 68 69 func NewSubscriberWithConnection(config Config, 70 logger watermill.LoggerAdapter, conn *ConnectionWrapper) (*Subscriber, error) { 71 if err := config.ValidateSubscriberWithConnection(); err != nil { 72 return nil, err 73 } 74 75 var closed atomic.Uint32 76 closedChan := make(chan struct{}) 77 var subscriberWaitGroup sync.WaitGroup 78 79 // Shared connections should not be closed by the subscriber. Just close the subscriber. 80 closeSubscriber := func() error { 81 if !closed.CompareAndSwap(0, 1) { 82 // Already closed. 83 return nil 84 } 85 86 logger.Debug("[Common] watermill amqp closing subscriber", nil) 87 88 close(closedChan) 89 90 subscriberWaitGroup.Wait() 91 92 return nil 93 } 94 95 return &Subscriber{ 96 ConnectionWrapper: conn, 97 config: config, 98 closedChan: closedChan, 99 closeSubscriber: closeSubscriber, 100 subscriberWaitGroup: &subscriberWaitGroup, 101 }, nil 102 } 103 104 // Subscribe consumes messages from AMQP broker. 105 // 106 // Watermill's topic in Subscribe is not mapped to AMQP's topic, but depending on configuration it can be mapped 107 // to exchange, queue or routing key. 108 // For detailed description of nomenclature mapping, please check "Nomenclature" paragraph in doc.go file. 109 func (s *Subscriber) Subscribe(ctx context.Context, topic string) (<-chan *message.Message, error) { 110 if s.Closed() { 111 return nil, errors.New("pub/sub is closedChan") 112 } 113 114 if !s.IsConnected() { 115 return nil, errors.New("not connected to AMQP") 116 } 117 118 logFields := watermill.LogFields{"topic": topic} 119 120 out := make(chan *message.Message) 121 122 queueName := s.config.Queue.GenerateName(topic) 123 logFields["amqp_queue_name"] = queueName 124 125 exchangeName := s.config.Exchange.GenerateName(topic) 126 logFields["amqp_exchange_name"] = exchangeName 127 128 if err := s.prepareConsume(queueName, exchangeName, logFields); err != nil { 129 return nil, errors.Wrap(err, "failed to prepare consume") 130 } 131 132 s.subscriberWaitGroup.Add(1) 133 s.connectionWaitGroup.Add(1) 134 135 go func(ctx context.Context) { 136 defer func() { 137 close(out) 138 s.logger.Info("[Common] watermill stopped consuming from AMQP channel", logFields) 139 s.connectionWaitGroup.Done() 140 s.subscriberWaitGroup.Done() 141 }() 142 143 ReconnectLoop: 144 for { 145 s.logger.Debug("[Common] watermill amqp waiting for s.connected or s.closing in reconnect loop", 146 logFields) 147 148 // to avoid race conditions with <-s.connected 149 select { 150 case <-s.closing: 151 s.logger.Debug("[Common] watermill amqp stopping reconnect loop (already closing)", logFields) 152 break ReconnectLoop 153 case <-s.closedChan: 154 s.logger.Debug("[Common] watermill amqp stopping reconnect loop (subscriber closing)", logFields) 155 break ReconnectLoop 156 default: 157 // not closing yet 158 } 159 160 select { 161 case <-s.connected: 162 s.logger.Debug("[Common] watermill amqp connection established in ReconnectLoop", logFields) 163 // runSubscriber blocks until connection fails or Close() is called 164 s.runSubscriber(ctx, out, queueName, exchangeName, logFields) 165 case <-s.closing: 166 s.logger.Debug("[Common] watermill amqp stopping reconnect loop (closing)", logFields) 167 break ReconnectLoop 168 case <-ctx.Done(): 169 s.logger.Debug("[Common] watermill amqp stopping reconnect loop (ctx done)", logFields) 170 break ReconnectLoop 171 } 172 173 time.Sleep(time.Millisecond * 100) 174 } 175 }(ctx) 176 177 return out, nil 178 } 179 180 func (s *Subscriber) SubscribeInitialize(topic string) (err error) { 181 if s.Closed() { 182 return errors.New("pub/sub is closed") 183 } 184 185 if !s.IsConnected() { 186 return errors.New("not connected to AMQP") 187 } 188 189 logFields := watermill.LogFields{"topic": topic} 190 191 queueName := s.config.Queue.GenerateName(topic) 192 logFields["amqp_queue_name"] = queueName 193 194 exchangeName := s.config.Exchange.GenerateName(topic) 195 logFields["amqp_exchange_name"] = exchangeName 196 197 s.logger.Info("[Common] watermill amqp initializing subscribe", logFields) 198 199 return errors.Wrap(s.prepareConsume(queueName, exchangeName, logFields), "failed to prepare consume") 200 } 201 202 // Close closes all subscriptions with their output channels. 203 func (s *Subscriber) Close() error { 204 return s.closeSubscriber() 205 } 206 207 func (s *Subscriber) prepareConsume(queueName string, exchangeName string, logFields watermill.LogFields) (err error) { 208 channel, err := s.openSubscribeChannel(logFields) 209 if err != nil { 210 return err 211 } 212 defer func() { 213 if channelCloseErr := channel.Close(); channelCloseErr != nil { 214 err = multierr.Append(err, channelCloseErr) 215 } 216 }() 217 218 if err = s.config.TopologyBuilder.BuildTopology(channel, queueName, exchangeName, s.config, s.logger); err != nil { 219 return err 220 } 221 222 s.logger.Debug("[Common] watermill amqp queue bound to exchange", logFields) 223 224 return nil 225 } 226 227 func (s *Subscriber) runSubscriber( 228 ctx context.Context, 229 out chan *message.Message, 230 queueName string, 231 exchangeName string, 232 logFields watermill.LogFields, 233 ) { 234 channel, err := s.openSubscribeChannel(logFields) 235 if err != nil { 236 s.logger.Error("[Common] watermill amqp failed to open channel", err, logFields) 237 return 238 } 239 defer func() { 240 if err := channel.Close(); err != nil { 241 s.logger.Error("[Common] watermill amqp failed to close channel", err, logFields) 242 } 243 }() 244 245 notifyCloseChannel := channel.NotifyClose(make(chan *amqp.Error, 1)) 246 247 sub := subscription{ 248 out: out, 249 logFields: logFields, 250 notifyCloseChannel: notifyCloseChannel, 251 channel: channel, 252 queueName: queueName, 253 logger: s.logger, 254 closing: s.closing, 255 closedChan: s.closedChan, 256 config: s.config, 257 } 258 259 s.logger.Info("[Common] watermill starting consuming from AMQP channel", logFields) 260 261 sub.ProcessMessages(ctx) 262 } 263 264 func (s *Subscriber) openSubscribeChannel(logFields watermill.LogFields) (*amqp.Channel, error) { 265 if !s.IsConnected() { 266 return nil, errors.New("not connected to AMQP") 267 } 268 269 channel, err := s.amqpConnection.Channel() 270 if err != nil { 271 return nil, errors.Wrap(err, "cannot open channel") 272 } 273 s.logger.Debug("[Common] watermill amqp channel opened", logFields) 274 275 if s.config.Consume.Qos != (QosConfig{}) { 276 if err := channel.Qos( 277 s.config.Consume.Qos.PrefetchCount, 278 s.config.Consume.Qos.PrefetchSize, 279 s.config.Consume.Qos.Global, 280 ); err != nil { 281 return nil, errors.Wrap(err, "failed to set channel Qos") 282 } 283 s.logger.Debug("[Common] watermill amqp qos set", logFields) 284 } 285 286 return channel, nil 287 } 288 289 type subscription struct { 290 out chan *message.Message 291 logFields watermill.LogFields 292 notifyCloseChannel chan *amqp.Error 293 channel *amqp.Channel 294 queueName string 295 296 logger watermill.LoggerAdapter 297 closing chan struct{} 298 closedChan chan struct{} 299 config Config 300 } 301 302 func (s *subscription) ProcessMessages(ctx context.Context) { 303 amqpMsgs, err := s.createConsumer(s.queueName, s.channel) 304 if err != nil { 305 s.logger.Error("[Common] watermill amqp failed to start consuming messages", err, s.logFields) 306 return 307 } 308 309 ConsumingLoop: 310 for { 311 select { 312 case amqpMsg := <-amqpMsgs: 313 if err := s.processMessage(ctx, amqpMsg, s.out, s.logFields); err != nil { 314 s.logger.Error("[Common] watermill amqp processing message failed, sending nack", err, s.logFields) 315 316 if err := s.nackMsg(amqpMsg); err != nil { 317 s.logger.Error("[Common] watermill amqp cannot nack message", err, s.logFields) 318 319 // something went really wrong when we cannot nack, let's reconnect 320 break ConsumingLoop 321 } 322 } 323 continue ConsumingLoop 324 325 case <-s.notifyCloseChannel: 326 s.logger.Error("[Common] watermill amqp channel closed, stopping process messages", nil, s.logFields) 327 break ConsumingLoop 328 329 case <-s.closing: 330 s.logger.Info("[Common] watermill amqp closing from subscriber received", s.logFields) 331 break ConsumingLoop 332 333 case <-s.closedChan: 334 s.logger.Info("[Common] watermill amqp subscriber closed", s.logFields) 335 break ConsumingLoop 336 337 case <-ctx.Done(): 338 s.logger.Info("[Common] watermill amqp closing from ctx received", s.logFields) 339 break ConsumingLoop 340 } 341 } 342 } 343 344 func (s *subscription) createConsumer(queueName string, channel *amqp.Channel) (<-chan amqp.Delivery, error) { 345 amqpMsgs, err := channel.Consume( 346 queueName, 347 s.config.Consume.Consumer, 348 false, // autoAck must be set to false - acks are managed by Watermill 349 s.config.Consume.Exclusive, 350 s.config.Consume.NoLocal, 351 s.config.Consume.NoWait, 352 s.config.Consume.Arguments, 353 ) 354 if err != nil { 355 return nil, errors.Wrap(err, "cannot consume from channel") 356 } 357 358 return amqpMsgs, nil 359 } 360 361 func (s *subscription) processMessage( 362 ctx context.Context, 363 amqpMsg amqp.Delivery, 364 out chan *message.Message, 365 logFields watermill.LogFields, 366 ) error { 367 msg, err := s.config.Marshaler.Unmarshal(amqpMsg) 368 if err != nil { 369 return err 370 } 371 372 ctx = context.WithValue(ctx, watermill.ContextKeyMessageUUID, msg.UUID) 373 ctx = context.WithValue(ctx, watermill.ContextKeyRawMessageID, amqpMsg.MessageId) 374 ctx, cancelCtx := context.WithCancel(ctx) 375 defer cancelCtx() 376 msg.Metadata[watermill.ContextKeyMessageUUID] = msg.UUID 377 msg.Metadata[watermill.ContextKeyRawMessageID] = amqpMsg.MessageId 378 msg.SetContext(ctx) 379 380 msgLogFields := logFields.Add(watermill.LogFields{ 381 "message_raw_id": amqpMsg.MessageId, 382 "message_uuid": msg.UUID, 383 }) 384 s.logger.Trace("[Common] watermill amqp unmarshaled message", msgLogFields) 385 386 select { 387 case <-s.closing: 388 s.logger.Info("[Common] watermill amqp message not consumed, pub/sub is closing", msgLogFields) 389 return s.nackMsg(amqpMsg) 390 case <-s.closedChan: 391 s.logger.Info("[Common] watermill amqp message not consumed, subscriber is closed", msgLogFields) 392 return s.nackMsg(amqpMsg) 393 case out <- msg: 394 s.logger.Trace("[Common] watermill amqp message sent to consumer", msgLogFields) 395 } 396 397 select { 398 case <-s.closing: 399 s.logger.Trace("[Common] watermill amqp closing pub/sub, message discarded before ack", msgLogFields) 400 return s.nackMsg(amqpMsg) 401 case <-s.closedChan: 402 s.logger.Info("[Common] watermill amqp message not consumed, subscriber is closed", msgLogFields) 403 return s.nackMsg(amqpMsg) 404 case <-msg.Acked(): 405 s.logger.Trace("[Common] watermill amqp message acked", msgLogFields) 406 return amqpMsg.Ack(false) 407 case <-msg.Nacked(): 408 s.logger.Trace("[Common] watermill amqp message nacked", msgLogFields) 409 return s.nackMsg(amqpMsg) 410 } 411 } 412 413 func (s *subscription) nackMsg(amqpMsg amqp.Delivery) error { 414 return amqpMsg.Nack(false, !s.config.Consume.NoRequeueOnNack) 415 }