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  }