github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/event/target/amqp.go (about)

     1  // Copyright (c) 2015-2023 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package target
    19  
    20  import (
    21  	"context"
    22  	"encoding/json"
    23  	"errors"
    24  	"fmt"
    25  	"net"
    26  	"net/url"
    27  	"os"
    28  	"path/filepath"
    29  	"sync"
    30  
    31  	"github.com/minio/minio/internal/event"
    32  	"github.com/minio/minio/internal/logger"
    33  	"github.com/minio/minio/internal/once"
    34  	"github.com/minio/minio/internal/store"
    35  	xnet "github.com/minio/pkg/v2/net"
    36  	"github.com/rabbitmq/amqp091-go"
    37  )
    38  
    39  // AMQPArgs - AMQP target arguments.
    40  type AMQPArgs struct {
    41  	Enable            bool     `json:"enable"`
    42  	URL               xnet.URL `json:"url"`
    43  	Exchange          string   `json:"exchange"`
    44  	RoutingKey        string   `json:"routingKey"`
    45  	ExchangeType      string   `json:"exchangeType"`
    46  	DeliveryMode      uint8    `json:"deliveryMode"`
    47  	Mandatory         bool     `json:"mandatory"`
    48  	Immediate         bool     `json:"immediate"`
    49  	Durable           bool     `json:"durable"`
    50  	Internal          bool     `json:"internal"`
    51  	NoWait            bool     `json:"noWait"`
    52  	AutoDeleted       bool     `json:"autoDeleted"`
    53  	PublisherConfirms bool     `json:"publisherConfirms"`
    54  	QueueDir          string   `json:"queueDir"`
    55  	QueueLimit        uint64   `json:"queueLimit"`
    56  }
    57  
    58  //lint:file-ignore ST1003 We cannot change these exported names.
    59  
    60  // AMQP input constants.
    61  const (
    62  	AmqpQueueDir   = "queue_dir"
    63  	AmqpQueueLimit = "queue_limit"
    64  
    65  	AmqpURL               = "url"
    66  	AmqpExchange          = "exchange"
    67  	AmqpRoutingKey        = "routing_key"
    68  	AmqpExchangeType      = "exchange_type"
    69  	AmqpDeliveryMode      = "delivery_mode"
    70  	AmqpMandatory         = "mandatory"
    71  	AmqpImmediate         = "immediate"
    72  	AmqpDurable           = "durable"
    73  	AmqpInternal          = "internal"
    74  	AmqpNoWait            = "no_wait"
    75  	AmqpAutoDeleted       = "auto_deleted"
    76  	AmqpArguments         = "arguments"
    77  	AmqpPublisherConfirms = "publisher_confirms"
    78  
    79  	EnvAMQPEnable            = "MINIO_NOTIFY_AMQP_ENABLE"
    80  	EnvAMQPURL               = "MINIO_NOTIFY_AMQP_URL"
    81  	EnvAMQPExchange          = "MINIO_NOTIFY_AMQP_EXCHANGE"
    82  	EnvAMQPRoutingKey        = "MINIO_NOTIFY_AMQP_ROUTING_KEY"
    83  	EnvAMQPExchangeType      = "MINIO_NOTIFY_AMQP_EXCHANGE_TYPE"
    84  	EnvAMQPDeliveryMode      = "MINIO_NOTIFY_AMQP_DELIVERY_MODE"
    85  	EnvAMQPMandatory         = "MINIO_NOTIFY_AMQP_MANDATORY"
    86  	EnvAMQPImmediate         = "MINIO_NOTIFY_AMQP_IMMEDIATE"
    87  	EnvAMQPDurable           = "MINIO_NOTIFY_AMQP_DURABLE"
    88  	EnvAMQPInternal          = "MINIO_NOTIFY_AMQP_INTERNAL"
    89  	EnvAMQPNoWait            = "MINIO_NOTIFY_AMQP_NO_WAIT"
    90  	EnvAMQPAutoDeleted       = "MINIO_NOTIFY_AMQP_AUTO_DELETED"
    91  	EnvAMQPArguments         = "MINIO_NOTIFY_AMQP_ARGUMENTS"
    92  	EnvAMQPPublisherConfirms = "MINIO_NOTIFY_AMQP_PUBLISHING_CONFIRMS"
    93  	EnvAMQPQueueDir          = "MINIO_NOTIFY_AMQP_QUEUE_DIR"
    94  	EnvAMQPQueueLimit        = "MINIO_NOTIFY_AMQP_QUEUE_LIMIT"
    95  )
    96  
    97  // Validate AMQP arguments
    98  func (a *AMQPArgs) Validate() error {
    99  	if !a.Enable {
   100  		return nil
   101  	}
   102  	if _, err := amqp091.ParseURI(a.URL.String()); err != nil {
   103  		return err
   104  	}
   105  	if a.QueueDir != "" {
   106  		if !filepath.IsAbs(a.QueueDir) {
   107  			return errors.New("queueDir path should be absolute")
   108  		}
   109  	}
   110  
   111  	return nil
   112  }
   113  
   114  // AMQPTarget - AMQP target
   115  type AMQPTarget struct {
   116  	initOnce once.Init
   117  
   118  	id         event.TargetID
   119  	args       AMQPArgs
   120  	conn       *amqp091.Connection
   121  	connMutex  sync.Mutex
   122  	store      store.Store[event.Event]
   123  	loggerOnce logger.LogOnce
   124  
   125  	quitCh chan struct{}
   126  }
   127  
   128  // ID - returns TargetID.
   129  func (target *AMQPTarget) ID() event.TargetID {
   130  	return target.id
   131  }
   132  
   133  // Name - returns the Name of the target.
   134  func (target *AMQPTarget) Name() string {
   135  	return target.ID().String()
   136  }
   137  
   138  // Store returns any underlying store if set.
   139  func (target *AMQPTarget) Store() event.TargetStore {
   140  	return target.store
   141  }
   142  
   143  // IsActive - Return true if target is up and active
   144  func (target *AMQPTarget) IsActive() (bool, error) {
   145  	if err := target.init(); err != nil {
   146  		return false, err
   147  	}
   148  
   149  	return target.isActive()
   150  }
   151  
   152  func (target *AMQPTarget) isActive() (bool, error) {
   153  	ch, _, err := target.channel()
   154  	if err != nil {
   155  		return false, err
   156  	}
   157  	defer func() {
   158  		ch.Close()
   159  	}()
   160  	return true, nil
   161  }
   162  
   163  func (target *AMQPTarget) channel() (*amqp091.Channel, chan amqp091.Confirmation, error) {
   164  	var err error
   165  	var conn *amqp091.Connection
   166  	var ch *amqp091.Channel
   167  
   168  	isAMQPClosedErr := func(err error) bool {
   169  		if err == amqp091.ErrClosed {
   170  			return true
   171  		}
   172  
   173  		if nerr, ok := err.(*net.OpError); ok {
   174  			return (nerr.Err.Error() == "use of closed network connection")
   175  		}
   176  
   177  		return false
   178  	}
   179  
   180  	target.connMutex.Lock()
   181  	defer target.connMutex.Unlock()
   182  
   183  	if target.conn != nil {
   184  		ch, err = target.conn.Channel()
   185  		if err == nil {
   186  			if target.args.PublisherConfirms {
   187  				confirms := ch.NotifyPublish(make(chan amqp091.Confirmation, 1))
   188  				if err := ch.Confirm(false); err != nil {
   189  					ch.Close()
   190  					return nil, nil, err
   191  				}
   192  				return ch, confirms, nil
   193  			}
   194  			return ch, nil, nil
   195  		}
   196  
   197  		if !isAMQPClosedErr(err) {
   198  			return nil, nil, err
   199  		}
   200  
   201  		// close when we know this is a network error.
   202  		target.conn.Close()
   203  	}
   204  
   205  	conn, err = amqp091.Dial(target.args.URL.String())
   206  	if err != nil {
   207  		if xnet.IsConnRefusedErr(err) {
   208  			return nil, nil, store.ErrNotConnected
   209  		}
   210  		return nil, nil, err
   211  	}
   212  
   213  	ch, err = conn.Channel()
   214  	if err != nil {
   215  		return nil, nil, err
   216  	}
   217  
   218  	target.conn = conn
   219  
   220  	if target.args.PublisherConfirms {
   221  		confirms := ch.NotifyPublish(make(chan amqp091.Confirmation, 1))
   222  		if err := ch.Confirm(false); err != nil {
   223  			ch.Close()
   224  			return nil, nil, err
   225  		}
   226  		return ch, confirms, nil
   227  	}
   228  
   229  	return ch, nil, nil
   230  }
   231  
   232  // send - sends an event to the AMQP091.
   233  func (target *AMQPTarget) send(eventData event.Event, ch *amqp091.Channel, confirms chan amqp091.Confirmation) error {
   234  	objectName, err := url.QueryUnescape(eventData.S3.Object.Key)
   235  	if err != nil {
   236  		return err
   237  	}
   238  	key := eventData.S3.Bucket.Name + "/" + objectName
   239  
   240  	data, err := json.Marshal(event.Log{EventName: eventData.EventName, Key: key, Records: []event.Event{eventData}})
   241  	if err != nil {
   242  		return err
   243  	}
   244  
   245  	headers := make(amqp091.Table)
   246  	// Add more information here as required, but be aware to not overload headers
   247  	headers["minio-bucket"] = eventData.S3.Bucket.Name
   248  	headers["minio-event"] = eventData.EventName.String()
   249  
   250  	if err = ch.ExchangeDeclare(target.args.Exchange, target.args.ExchangeType, target.args.Durable,
   251  		target.args.AutoDeleted, target.args.Internal, target.args.NoWait, nil); err != nil {
   252  		return err
   253  	}
   254  
   255  	if err = ch.Publish(target.args.Exchange, target.args.RoutingKey, target.args.Mandatory,
   256  		target.args.Immediate, amqp091.Publishing{
   257  			Headers:      headers,
   258  			ContentType:  "application/json",
   259  			DeliveryMode: target.args.DeliveryMode,
   260  			Body:         data,
   261  		}); err != nil {
   262  		return err
   263  	}
   264  
   265  	// check for publisher confirms only if its enabled
   266  	if target.args.PublisherConfirms {
   267  		confirmed := <-confirms
   268  		if !confirmed.Ack {
   269  			return fmt.Errorf("failed delivery of delivery tag: %d", confirmed.DeliveryTag)
   270  		}
   271  	}
   272  
   273  	return nil
   274  }
   275  
   276  // Save - saves the events to the store which will be replayed when the amqp connection is active.
   277  func (target *AMQPTarget) Save(eventData event.Event) error {
   278  	if target.store != nil {
   279  		return target.store.Put(eventData)
   280  	}
   281  	if err := target.init(); err != nil {
   282  		return err
   283  	}
   284  	ch, confirms, err := target.channel()
   285  	if err != nil {
   286  		return err
   287  	}
   288  	defer ch.Close()
   289  
   290  	return target.send(eventData, ch, confirms)
   291  }
   292  
   293  // SendFromStore - reads an event from store and sends it to AMQP091.
   294  func (target *AMQPTarget) SendFromStore(key store.Key) error {
   295  	if err := target.init(); err != nil {
   296  		return err
   297  	}
   298  
   299  	ch, confirms, err := target.channel()
   300  	if err != nil {
   301  		return err
   302  	}
   303  	defer ch.Close()
   304  
   305  	eventData, eErr := target.store.Get(key.Name)
   306  	if eErr != nil {
   307  		// The last event key in a successful batch will be sent in the channel atmost once by the replayEvents()
   308  		// Such events will not exist and wouldve been already been sent successfully.
   309  		if os.IsNotExist(eErr) {
   310  			return nil
   311  		}
   312  		return eErr
   313  	}
   314  
   315  	if err := target.send(eventData, ch, confirms); err != nil {
   316  		return err
   317  	}
   318  
   319  	// Delete the event from store.
   320  	return target.store.Del(key.Name)
   321  }
   322  
   323  // Close - does nothing and available for interface compatibility.
   324  func (target *AMQPTarget) Close() error {
   325  	close(target.quitCh)
   326  	if target.conn != nil {
   327  		return target.conn.Close()
   328  	}
   329  	return nil
   330  }
   331  
   332  func (target *AMQPTarget) init() error {
   333  	return target.initOnce.Do(target.initAMQP)
   334  }
   335  
   336  func (target *AMQPTarget) initAMQP() error {
   337  	conn, err := amqp091.Dial(target.args.URL.String())
   338  	if err != nil {
   339  		if xnet.IsConnRefusedErr(err) || xnet.IsConnResetErr(err) {
   340  			target.loggerOnce(context.Background(), err, target.ID().String())
   341  		}
   342  		return err
   343  	}
   344  	target.conn = conn
   345  
   346  	return nil
   347  }
   348  
   349  // NewAMQPTarget - creates new AMQP target.
   350  func NewAMQPTarget(id string, args AMQPArgs, loggerOnce logger.LogOnce) (*AMQPTarget, error) {
   351  	var queueStore store.Store[event.Event]
   352  	if args.QueueDir != "" {
   353  		queueDir := filepath.Join(args.QueueDir, storePrefix+"-amqp-"+id)
   354  		queueStore = store.NewQueueStore[event.Event](queueDir, args.QueueLimit, event.StoreExtension)
   355  		if err := queueStore.Open(); err != nil {
   356  			return nil, fmt.Errorf("unable to initialize the queue store of AMQP `%s`: %w", id, err)
   357  		}
   358  	}
   359  
   360  	target := &AMQPTarget{
   361  		id:         event.TargetID{ID: id, Name: "amqp"},
   362  		args:       args,
   363  		loggerOnce: loggerOnce,
   364  		store:      queueStore,
   365  		quitCh:     make(chan struct{}),
   366  	}
   367  
   368  	if target.store != nil {
   369  		store.StreamItems(target.store, target, target.quitCh, target.loggerOnce)
   370  	}
   371  
   372  	return target, nil
   373  }