github.com/wfusion/gofusion@v1.1.14/common/infra/watermill/pubsub/amqp/publisher.go (about)

     1  package amqp
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sync"
     7  
     8  	"github.com/pkg/errors"
     9  	"go.uber.org/multierr"
    10  
    11  	"github.com/wfusion/gofusion/common/infra/watermill"
    12  	"github.com/wfusion/gofusion/common/infra/watermill/message"
    13  
    14  	amqp "github.com/rabbitmq/amqp091-go"
    15  )
    16  
    17  type Publisher struct {
    18  	*ConnectionWrapper
    19  
    20  	config                  Config
    21  	publishBindingsLock     sync.RWMutex
    22  	publishBindingsPrepared map[string]struct{}
    23  	closePublisher          func() error
    24  	chanProvider            channelProvider
    25  }
    26  
    27  func NewPublisher(config Config, logger watermill.LoggerAdapter) (*Publisher, error) {
    28  	if err := config.ValidatePublisher(); err != nil {
    29  		return nil, err
    30  	}
    31  
    32  	var err error
    33  
    34  	conn, err := NewConnection(config.Connection, logger)
    35  	if err != nil {
    36  		return nil, fmt.Errorf("create new connection: %w", err)
    37  	}
    38  
    39  	chanProvider, err := newChannelProvider(conn, config.Publish.ChannelPoolSize,
    40  		config.Publish.ConfirmDelivery, logger)
    41  	if err != nil {
    42  		return nil, fmt.Errorf("create new channel pool: %w", err)
    43  	}
    44  
    45  	// Close the connection when the publisher is closed since this publisher owns the connection.
    46  	closePublisher := func() error {
    47  		logger.Debug("[Common] watermill amqp closing publisher connection", nil)
    48  
    49  		chanProvider.Close()
    50  
    51  		return conn.Close()
    52  	}
    53  
    54  	return &Publisher{
    55  		ConnectionWrapper:       conn,
    56  		config:                  config,
    57  		publishBindingsPrepared: make(map[string]struct{}),
    58  		closePublisher:          closePublisher,
    59  		chanProvider:            chanProvider,
    60  	}, nil
    61  }
    62  
    63  func NewPublisherWithConnection(config Config, logger watermill.LoggerAdapter,
    64  	conn *ConnectionWrapper) (*Publisher, error) {
    65  	if err := config.ValidatePublisherWithConnection(); err != nil {
    66  		return nil, err
    67  	}
    68  
    69  	chanProvider, err := newChannelProvider(conn,
    70  		config.Publish.ChannelPoolSize, config.Publish.ConfirmDelivery, logger)
    71  	if err != nil {
    72  		return nil, fmt.Errorf("create new channel pool: %w", err)
    73  	}
    74  
    75  	// Shared connections should not be closed by the publisher.
    76  	closePublisher := func() error {
    77  		logger.Debug("[Common] watermill amqp publisher closed", nil)
    78  
    79  		chanProvider.Close()
    80  
    81  		return nil
    82  	}
    83  
    84  	return &Publisher{
    85  		ConnectionWrapper:       conn,
    86  		config:                  config,
    87  		publishBindingsPrepared: make(map[string]struct{}),
    88  		closePublisher:          closePublisher,
    89  		chanProvider:            chanProvider,
    90  	}, nil
    91  }
    92  
    93  // Publish publishes messages to AMQP broker.
    94  // Publish is blocking until the broker has received and saved the message.
    95  // Publish is always thread safe.
    96  //
    97  // Watermill's topic in Publish is not mapped to AMQP's topic, but depending on configuration it can be mapped
    98  // to exchange, queue or routing key.
    99  // For detailed description of nomenclature mapping, please check "Nomenclature" paragraph in doc.go file.
   100  func (p *Publisher) Publish(ctx context.Context, topic string, messages ...*message.Message) (err error) {
   101  	if p.Closed() {
   102  		return errors.New("pub/sub is connection closedChan")
   103  	}
   104  
   105  	if !p.IsConnected() {
   106  		return errors.New("not connected to AMQP")
   107  	}
   108  
   109  	p.connectionWaitGroup.Add(1)
   110  	defer p.connectionWaitGroup.Done()
   111  
   112  	c, err := p.chanProvider.Channel()
   113  	if err != nil {
   114  		return errors.Wrap(err, "cannot open channel")
   115  	}
   116  	defer func() {
   117  		if channelCloseErr := p.chanProvider.CloseChannel(c); channelCloseErr != nil {
   118  			err = multierr.Append(err, channelCloseErr)
   119  		}
   120  	}()
   121  
   122  	channel := c.AMQPChannel()
   123  
   124  	if p.config.Publish.Transactional {
   125  		if err := p.beginTransaction(channel); err != nil {
   126  			return err
   127  		}
   128  
   129  		defer func() {
   130  			err = p.commitTransaction(channel, err)
   131  		}()
   132  	}
   133  
   134  	if err := p.preparePublishBindings(topic, channel); err != nil {
   135  		return err
   136  	}
   137  
   138  	logFields := make(watermill.LogFields, 3)
   139  
   140  	exchangeName := p.config.Exchange.GenerateName(topic)
   141  	logFields["amqp_exchange_name"] = exchangeName
   142  
   143  	routingKey := p.config.Publish.GenerateRoutingKey(topic)
   144  	logFields["amqp_routing_key"] = routingKey
   145  
   146  	for _, msg := range messages {
   147  		if err := p.publishMessage(ctx, exchangeName, routingKey, msg, c, logFields); err != nil {
   148  			return err
   149  		}
   150  	}
   151  
   152  	return nil
   153  }
   154  
   155  func (p *Publisher) Close() error {
   156  	return p.closePublisher()
   157  }
   158  
   159  func (p *Publisher) beginTransaction(channel *amqp.Channel) error {
   160  	if err := channel.Tx(); err != nil {
   161  		return errors.Wrap(err, "cannot start transaction")
   162  	}
   163  
   164  	p.logger.Trace("Transaction begun", nil)
   165  
   166  	return nil
   167  }
   168  
   169  func (p *Publisher) commitTransaction(channel *amqp.Channel, err error) error {
   170  	if err != nil {
   171  		if rollbackErr := channel.TxRollback(); rollbackErr != nil {
   172  			return multierr.Append(err, rollbackErr)
   173  		}
   174  	}
   175  
   176  	return channel.TxCommit()
   177  }
   178  
   179  func (p *Publisher) publishMessage(
   180  	ctx context.Context,
   181  	exchangeName, routingKey string,
   182  	msg *message.Message,
   183  	channel channel,
   184  	logFields watermill.LogFields,
   185  ) error {
   186  	logFields = logFields.Add(watermill.LogFields{"message_uuid": msg.UUID})
   187  
   188  	p.logger.Trace("[Common] watermill amqp publishing message", logFields)
   189  
   190  	amqpMsg, err := p.config.Marshaler.Marshal(msg)
   191  	if err != nil {
   192  		return errors.Wrap(err, "cannot marshal message")
   193  	}
   194  
   195  	if err = channel.AMQPChannel().PublishWithContext(
   196  		ctx,
   197  		exchangeName,
   198  		routingKey,
   199  		p.config.Publish.Mandatory,
   200  		p.config.Publish.Immediate,
   201  		amqpMsg,
   202  	); err != nil {
   203  		return errors.Wrap(err, "cannot publish msg")
   204  	}
   205  
   206  	if !channel.DeliveryConfirmationEnabled() {
   207  		p.logger.Trace("[Common] watermill amqp message published", logFields)
   208  
   209  		return nil
   210  	}
   211  
   212  	p.logger.Trace("[Common] watermill message published. waiting for delivery confirmation.", logFields)
   213  
   214  	if !channel.Delivered() {
   215  		return fmt.Errorf("delivery not confirmed for message [%s]", msg.UUID)
   216  	}
   217  
   218  	p.logger.Trace("[Common] watermill delivery confirmed for message", logFields)
   219  
   220  	return nil
   221  }
   222  
   223  func (p *Publisher) preparePublishBindings(topic string, channel *amqp.Channel) error {
   224  	p.publishBindingsLock.RLock()
   225  	_, prepared := p.publishBindingsPrepared[topic]
   226  	p.publishBindingsLock.RUnlock()
   227  
   228  	if prepared {
   229  		return nil
   230  	}
   231  
   232  	p.publishBindingsLock.Lock()
   233  	defer p.publishBindingsLock.Unlock()
   234  
   235  	if p.config.Exchange.GenerateName(topic) != "" {
   236  		if err := p.config.TopologyBuilder.ExchangeDeclare(
   237  			channel, p.config.Exchange.GenerateName(topic), p.config); err != nil {
   238  			return err
   239  		}
   240  	}
   241  
   242  	p.publishBindingsPrepared[topic] = struct{}{}
   243  
   244  	return nil
   245  }