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 }