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  //------------------------------------------------------------------------------