github.com/Jeffail/benthos/v3@v3.65.0/lib/input/reader/amqp_0_9.go (about) 1 package reader 2 3 import ( 4 "context" 5 "crypto/tls" 6 "errors" 7 "fmt" 8 "net/url" 9 "regexp" 10 "strings" 11 "sync" 12 "time" 13 14 "github.com/Jeffail/benthos/v3/lib/log" 15 "github.com/Jeffail/benthos/v3/lib/message" 16 "github.com/Jeffail/benthos/v3/lib/message/batch" 17 "github.com/Jeffail/benthos/v3/lib/metrics" 18 "github.com/Jeffail/benthos/v3/lib/types" 19 btls "github.com/Jeffail/benthos/v3/lib/util/tls" 20 amqp "github.com/rabbitmq/amqp091-go" 21 ) 22 23 const defaultDeprecatedAMQP09URL = "amqp://guest:guest@localhost:5672/" 24 25 var errAMQP09Connect = errors.New("AMQP 0.9 Connect") 26 27 // AMQP09QueueDeclareConfig contains fields indicating whether the target AMQP09 28 // queue needs to be declared and bound to an exchange, as well as any fields 29 // specifying how to accomplish that. 30 type AMQP09QueueDeclareConfig struct { 31 Enabled bool `json:"enabled" yaml:"enabled"` 32 Durable bool `json:"durable" yaml:"durable"` 33 } 34 35 // AMQP09BindingConfig contains fields describing a queue binding to be 36 // declared. 37 type AMQP09BindingConfig struct { 38 Exchange string `json:"exchange" yaml:"exchange"` 39 RoutingKey string `json:"key" yaml:"key"` 40 } 41 42 // AMQP09Config contains configuration for the AMQP09 input type. 43 type AMQP09Config struct { 44 URL string `json:"url" yaml:"url"` 45 URLs []string `json:"urls" yaml:"urls"` 46 Queue string `json:"queue" yaml:"queue"` 47 QueueDeclare AMQP09QueueDeclareConfig `json:"queue_declare" yaml:"queue_declare"` 48 BindingsDeclare []AMQP09BindingConfig `json:"bindings_declare" yaml:"bindings_declare"` 49 ConsumerTag string `json:"consumer_tag" yaml:"consumer_tag"` 50 AutoAck bool `json:"auto_ack" yaml:"auto_ack"` 51 NackRejectPatterns []string `json:"nack_reject_patterns" yaml:"nack_reject_patterns"` 52 PrefetchCount int `json:"prefetch_count" yaml:"prefetch_count"` 53 PrefetchSize int `json:"prefetch_size" yaml:"prefetch_size"` 54 TLS btls.Config `json:"tls" yaml:"tls"` 55 56 // TODO: V4 remove this (maybe in V5 to allow a grace period) 57 Batching batch.PolicyConfig `json:"batching" yaml:"batching"` 58 } 59 60 // NewAMQP09Config creates a new AMQP09Config with default values. 61 func NewAMQP09Config() AMQP09Config { 62 return AMQP09Config{ 63 URL: defaultDeprecatedAMQP09URL, 64 URLs: []string{}, 65 Queue: "benthos-queue", 66 QueueDeclare: AMQP09QueueDeclareConfig{ 67 Enabled: false, 68 Durable: true, 69 }, 70 ConsumerTag: "benthos-consumer", 71 AutoAck: false, 72 NackRejectPatterns: []string{}, 73 PrefetchCount: 10, 74 PrefetchSize: 0, 75 TLS: btls.NewConfig(), 76 Batching: batch.NewPolicyConfig(), 77 BindingsDeclare: []AMQP09BindingConfig{}, 78 } 79 } 80 81 //------------------------------------------------------------------------------ 82 83 // AMQP09 is an input type that reads messages via the AMQP09 0.9 protocol. 84 type AMQP09 struct { 85 conn *amqp.Connection 86 amqpChan *amqp.Channel 87 consumerChan <-chan amqp.Delivery 88 89 urls []string 90 tlsConf *tls.Config 91 92 nackRejectPattens []*regexp.Regexp 93 94 conf AMQP09Config 95 stats metrics.Type 96 log log.Modular 97 98 m sync.RWMutex 99 } 100 101 // NewAMQP09 creates a new AMQP09 input type. 102 func NewAMQP09(conf AMQP09Config, log log.Modular, stats metrics.Type) (*AMQP09, error) { 103 a := AMQP09{ 104 conf: conf, 105 stats: stats, 106 log: log, 107 } 108 109 if conf.URL != defaultDeprecatedAMQP09URL && len(conf.URLs) > 0 { 110 return nil, errors.New("cannot mix both the field `url` and `urls`") 111 } 112 113 if len(conf.URLs) == 0 { 114 a.urls = append(a.urls, conf.URL) 115 } 116 117 for _, u := range conf.URLs { 118 for _, splitURL := range strings.Split(u, ",") { 119 if trimmed := strings.TrimSpace(splitURL); len(trimmed) > 0 { 120 a.urls = append(a.urls, trimmed) 121 } 122 } 123 } 124 125 for _, p := range conf.NackRejectPatterns { 126 r, err := regexp.Compile(p) 127 if err != nil { 128 return nil, fmt.Errorf("failed to compile nack reject pattern: %w", err) 129 } 130 a.nackRejectPattens = append(a.nackRejectPattens, r) 131 } 132 133 if conf.TLS.Enabled { 134 var err error 135 if a.tlsConf, err = conf.TLS.Get(); err != nil { 136 return nil, err 137 } 138 } 139 return &a, nil 140 } 141 142 //------------------------------------------------------------------------------ 143 144 // ConnectWithContext establishes a connection to an AMQP09 server. 145 func (a *AMQP09) ConnectWithContext(ctx context.Context) (err error) { 146 a.m.Lock() 147 defer a.m.Unlock() 148 149 if a.conn != nil { 150 return nil 151 } 152 153 var conn *amqp.Connection 154 var amqpChan *amqp.Channel 155 var consumerChan <-chan amqp.Delivery 156 157 if conn, err = a.reDial(a.urls); err != nil { 158 return err 159 } 160 161 amqpChan, err = conn.Channel() 162 if err != nil { 163 return fmt.Errorf("AMQP 0.9 Channel: %s", err) 164 } 165 166 if a.conf.QueueDeclare.Enabled { 167 if _, err = amqpChan.QueueDeclare( 168 a.conf.Queue, // name of the queue 169 a.conf.QueueDeclare.Durable, // durable 170 false, // delete when unused 171 false, // exclusive 172 false, // noWait 173 nil, // arguments 174 ); err != nil { 175 return fmt.Errorf("queue Declare: %s", err) 176 } 177 } 178 179 for _, bConf := range a.conf.BindingsDeclare { 180 if err = amqpChan.QueueBind( 181 a.conf.Queue, // name of the queue 182 bConf.RoutingKey, // bindingKey 183 bConf.Exchange, // sourceExchange 184 false, // noWait 185 nil, // arguments 186 ); err != nil { 187 return fmt.Errorf("queue Bind: %s", err) 188 } 189 } 190 191 if err = amqpChan.Qos( 192 a.conf.PrefetchCount, a.conf.PrefetchSize, false, 193 ); err != nil { 194 return fmt.Errorf("qos: %s", err) 195 } 196 197 if consumerChan, err = amqpChan.Consume( 198 a.conf.Queue, // name 199 a.conf.ConsumerTag, // consumerTag, 200 a.conf.AutoAck, // autoAck 201 false, // exclusive 202 false, // noLocal 203 false, // noWait 204 nil, // arguments 205 ); err != nil { 206 return fmt.Errorf("queue Consume: %s", err) 207 } 208 209 a.conn = conn 210 a.amqpChan = amqpChan 211 a.consumerChan = consumerChan 212 213 a.log.Infof("Receiving AMQP 0.9 messages from queue: %v\n", a.conf.Queue) 214 return 215 } 216 217 // disconnect safely closes a connection to an AMQP09 server. 218 func (a *AMQP09) disconnect() error { 219 a.m.Lock() 220 defer a.m.Unlock() 221 222 if a.amqpChan != nil { 223 if err := a.amqpChan.Cancel(a.conf.ConsumerTag, true); err != nil { 224 a.log.Errorf("Failed to cancel consumer: %v\n", err) 225 } 226 a.amqpChan = nil 227 } 228 if a.conn != nil { 229 if err := a.conn.Close(); err != nil { 230 a.log.Errorf("Failed to close connection cleanly: %v\n", err) 231 } 232 a.conn = nil 233 } 234 235 return nil 236 } 237 238 //------------------------------------------------------------------------------ 239 240 // ReadWithContext a new AMQP09 message. 241 func (a *AMQP09) ReadWithContext(ctx context.Context) (types.Message, AsyncAckFn, error) { 242 var c <-chan amqp.Delivery 243 244 a.m.RLock() 245 if a.conn != nil { 246 c = a.consumerChan 247 } 248 a.m.RUnlock() 249 250 if c == nil { 251 return nil, nil, types.ErrNotConnected 252 } 253 254 msg := message.New(nil) 255 addPart := func(data amqp.Delivery) { 256 part := message.NewPart(data.Body) 257 258 for k, v := range data.Headers { 259 setMetadata(part, k, v) 260 } 261 262 setMetadata(part, "amqp_content_type", data.ContentType) 263 setMetadata(part, "amqp_content_encoding", data.ContentEncoding) 264 265 if data.DeliveryMode != 0 { 266 setMetadata(part, "amqp_delivery_mode", data.DeliveryMode) 267 } 268 269 setMetadata(part, "amqp_priority", data.Priority) 270 setMetadata(part, "amqp_correlation_id", data.CorrelationId) 271 setMetadata(part, "amqp_reply_to", data.ReplyTo) 272 setMetadata(part, "amqp_expiration", data.Expiration) 273 setMetadata(part, "amqp_message_id", data.MessageId) 274 275 if !data.Timestamp.IsZero() { 276 setMetadata(part, "amqp_timestamp", data.Timestamp.Unix()) 277 } 278 279 setMetadata(part, "amqp_type", data.Type) 280 setMetadata(part, "amqp_user_id", data.UserId) 281 setMetadata(part, "amqp_app_id", data.AppId) 282 setMetadata(part, "amqp_consumer_tag", data.ConsumerTag) 283 setMetadata(part, "amqp_delivery_tag", data.DeliveryTag) 284 setMetadata(part, "amqp_redelivered", data.Redelivered) 285 setMetadata(part, "amqp_exchange", data.Exchange) 286 setMetadata(part, "amqp_routing_key", data.RoutingKey) 287 288 msg.Append(part) 289 } 290 291 select { 292 case data, open := <-c: 293 if !open { 294 a.disconnect() 295 return nil, nil, types.ErrNotConnected 296 } 297 addPart(data) 298 return msg, func(actx context.Context, res types.Response) error { 299 if a.conf.AutoAck { 300 return nil 301 } 302 if res.Error() != nil { 303 errStr := res.Error().Error() 304 for _, p := range a.nackRejectPattens { 305 if p.MatchString(errStr) { 306 return data.Nack(false, false) 307 } 308 } 309 return data.Nack(false, true) 310 } 311 return data.Ack(false) 312 }, nil 313 case <-ctx.Done(): 314 } 315 return nil, nil, types.ErrTimeout 316 } 317 318 // CloseAsync shuts down the AMQP09 input and stops processing requests. 319 func (a *AMQP09) CloseAsync() { 320 a.disconnect() 321 } 322 323 // WaitForClose blocks until the AMQP09 input has closed down. 324 func (a *AMQP09) WaitForClose(timeout time.Duration) error { 325 return nil 326 } 327 328 // reDial connection to amqp with one or more fallback URLs 329 func (a *AMQP09) reDial(urls []string) (conn *amqp.Connection, err error) { 330 for _, u := range urls { 331 conn, err = a.dial(u) 332 if err != nil { 333 if errors.Is(err, errAMQP09Connect) { 334 continue 335 } 336 break 337 } 338 return conn, nil 339 } 340 return nil, err 341 } 342 343 // dial attempts to connect to amqp URL 344 func (a *AMQP09) dial(amqpURL string) (conn *amqp.Connection, err error) { 345 u, err := url.Parse(amqpURL) 346 if err != nil { 347 return nil, fmt.Errorf("invalid AMQP URL: %w", err) 348 } 349 350 if a.conf.TLS.Enabled { 351 if u.User != nil { 352 conn, err = amqp.DialTLS(amqpURL, a.tlsConf) 353 if err != nil { 354 return nil, fmt.Errorf("%w: %s", errAMQP09Connect, err) 355 } 356 } else { 357 conn, err = amqp.DialTLS_ExternalAuth(amqpURL, a.tlsConf) 358 if err != nil { 359 return nil, fmt.Errorf("%w: %s", errAMQP09Connect, err) 360 } 361 } 362 } else { 363 conn, err = amqp.Dial(amqpURL) 364 if err != nil { 365 return nil, fmt.Errorf("%w: %s", errAMQP09Connect, err) 366 } 367 } 368 369 return conn, nil 370 } 371 372 //------------------------------------------------------------------------------