storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/pkg/event/target/amqp.go (about)

     1  /*
     2   * MinIO Cloud Storage, (C) 2018 MinIO, Inc.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package target
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"errors"
    23  	"net"
    24  	"net/url"
    25  	"os"
    26  	"path/filepath"
    27  	"sync"
    28  
    29  	"github.com/streadway/amqp"
    30  
    31  	"storj.io/minio/pkg/event"
    32  	xnet "storj.io/minio/pkg/net"
    33  )
    34  
    35  // AMQPArgs - AMQP target arguments.
    36  type AMQPArgs struct {
    37  	Enable       bool     `json:"enable"`
    38  	URL          xnet.URL `json:"url"`
    39  	Exchange     string   `json:"exchange"`
    40  	RoutingKey   string   `json:"routingKey"`
    41  	ExchangeType string   `json:"exchangeType"`
    42  	DeliveryMode uint8    `json:"deliveryMode"`
    43  	Mandatory    bool     `json:"mandatory"`
    44  	Immediate    bool     `json:"immediate"`
    45  	Durable      bool     `json:"durable"`
    46  	Internal     bool     `json:"internal"`
    47  	NoWait       bool     `json:"noWait"`
    48  	AutoDeleted  bool     `json:"autoDeleted"`
    49  	QueueDir     string   `json:"queueDir"`
    50  	QueueLimit   uint64   `json:"queueLimit"`
    51  }
    52  
    53  //lint:file-ignore ST1003 We cannot change these exported names.
    54  
    55  // AMQP input constants.
    56  const (
    57  	AmqpQueueDir   = "queue_dir"
    58  	AmqpQueueLimit = "queue_limit"
    59  
    60  	AmqpURL               = "url"
    61  	AmqpExchange          = "exchange"
    62  	AmqpRoutingKey        = "routing_key"
    63  	AmqpExchangeType      = "exchange_type"
    64  	AmqpDeliveryMode      = "delivery_mode"
    65  	AmqpMandatory         = "mandatory"
    66  	AmqpImmediate         = "immediate"
    67  	AmqpDurable           = "durable"
    68  	AmqpInternal          = "internal"
    69  	AmqpNoWait            = "no_wait"
    70  	AmqpAutoDeleted       = "auto_deleted"
    71  	AmqpArguments         = "arguments"
    72  	AmqpPublishingHeaders = "publishing_headers"
    73  
    74  	EnvAMQPEnable            = "MINIO_NOTIFY_AMQP_ENABLE"
    75  	EnvAMQPURL               = "MINIO_NOTIFY_AMQP_URL"
    76  	EnvAMQPExchange          = "MINIO_NOTIFY_AMQP_EXCHANGE"
    77  	EnvAMQPRoutingKey        = "MINIO_NOTIFY_AMQP_ROUTING_KEY"
    78  	EnvAMQPExchangeType      = "MINIO_NOTIFY_AMQP_EXCHANGE_TYPE"
    79  	EnvAMQPDeliveryMode      = "MINIO_NOTIFY_AMQP_DELIVERY_MODE"
    80  	EnvAMQPMandatory         = "MINIO_NOTIFY_AMQP_MANDATORY"
    81  	EnvAMQPImmediate         = "MINIO_NOTIFY_AMQP_IMMEDIATE"
    82  	EnvAMQPDurable           = "MINIO_NOTIFY_AMQP_DURABLE"
    83  	EnvAMQPInternal          = "MINIO_NOTIFY_AMQP_INTERNAL"
    84  	EnvAMQPNoWait            = "MINIO_NOTIFY_AMQP_NO_WAIT"
    85  	EnvAMQPAutoDeleted       = "MINIO_NOTIFY_AMQP_AUTO_DELETED"
    86  	EnvAMQPArguments         = "MINIO_NOTIFY_AMQP_ARGUMENTS"
    87  	EnvAMQPPublishingHeaders = "MINIO_NOTIFY_AMQP_PUBLISHING_HEADERS"
    88  	EnvAMQPQueueDir          = "MINIO_NOTIFY_AMQP_QUEUE_DIR"
    89  	EnvAMQPQueueLimit        = "MINIO_NOTIFY_AMQP_QUEUE_LIMIT"
    90  )
    91  
    92  // Validate AMQP arguments
    93  func (a *AMQPArgs) Validate() error {
    94  	if !a.Enable {
    95  		return nil
    96  	}
    97  	if _, err := amqp.ParseURI(a.URL.String()); err != nil {
    98  		return err
    99  	}
   100  	if a.QueueDir != "" {
   101  		if !filepath.IsAbs(a.QueueDir) {
   102  			return errors.New("queueDir path should be absolute")
   103  		}
   104  	}
   105  
   106  	return nil
   107  }
   108  
   109  // AMQPTarget - AMQP target
   110  type AMQPTarget struct {
   111  	id         event.TargetID
   112  	args       AMQPArgs
   113  	conn       *amqp.Connection
   114  	connMutex  sync.Mutex
   115  	store      Store
   116  	loggerOnce func(ctx context.Context, err error, id interface{}, errKind ...interface{})
   117  }
   118  
   119  // ID - returns TargetID.
   120  func (target *AMQPTarget) ID() event.TargetID {
   121  	return target.id
   122  }
   123  
   124  // IsActive - Return true if target is up and active
   125  func (target *AMQPTarget) IsActive() (bool, error) {
   126  	ch, err := target.channel()
   127  	if err != nil {
   128  		return false, err
   129  	}
   130  	defer func() {
   131  		ch.Close()
   132  	}()
   133  	return true, nil
   134  }
   135  
   136  // HasQueueStore - Checks if the queueStore has been configured for the target
   137  func (target *AMQPTarget) HasQueueStore() bool {
   138  	return target.store != nil
   139  }
   140  
   141  func (target *AMQPTarget) channel() (*amqp.Channel, error) {
   142  	var err error
   143  	var conn *amqp.Connection
   144  	var ch *amqp.Channel
   145  
   146  	isAMQPClosedErr := func(err error) bool {
   147  		if err == amqp.ErrClosed {
   148  			return true
   149  		}
   150  
   151  		if nerr, ok := err.(*net.OpError); ok {
   152  			return (nerr.Err.Error() == "use of closed network connection")
   153  		}
   154  
   155  		return false
   156  	}
   157  
   158  	target.connMutex.Lock()
   159  	defer target.connMutex.Unlock()
   160  
   161  	if target.conn != nil {
   162  		ch, err = target.conn.Channel()
   163  		if err == nil {
   164  			return ch, nil
   165  		}
   166  
   167  		if !isAMQPClosedErr(err) {
   168  			return nil, err
   169  		}
   170  	}
   171  
   172  	conn, err = amqp.Dial(target.args.URL.String())
   173  	if err != nil {
   174  		if IsConnRefusedErr(err) {
   175  			return nil, errNotConnected
   176  		}
   177  		return nil, err
   178  	}
   179  
   180  	ch, err = conn.Channel()
   181  	if err != nil {
   182  		return nil, err
   183  	}
   184  
   185  	target.conn = conn
   186  
   187  	return ch, nil
   188  }
   189  
   190  // send - sends an event to the AMQP.
   191  func (target *AMQPTarget) send(eventData event.Event, ch *amqp.Channel) error {
   192  	objectName, err := url.QueryUnescape(eventData.S3.Object.Key)
   193  	if err != nil {
   194  		return err
   195  	}
   196  	key := eventData.S3.Bucket.Name + "/" + objectName
   197  
   198  	data, err := json.Marshal(event.Log{EventName: eventData.EventName, Key: key, Records: []event.Event{eventData}})
   199  	if err != nil {
   200  		return err
   201  	}
   202  
   203  	if err = ch.ExchangeDeclare(target.args.Exchange, target.args.ExchangeType, target.args.Durable,
   204  		target.args.AutoDeleted, target.args.Internal, target.args.NoWait, nil); err != nil {
   205  		return err
   206  	}
   207  
   208  	if err := ch.Publish(target.args.Exchange, target.args.RoutingKey, target.args.Mandatory,
   209  		target.args.Immediate, amqp.Publishing{
   210  			ContentType:  "application/json",
   211  			DeliveryMode: target.args.DeliveryMode,
   212  			Body:         data,
   213  		}); err != nil {
   214  		return err
   215  	}
   216  
   217  	return nil
   218  }
   219  
   220  // Save - saves the events to the store which will be replayed when the amqp connection is active.
   221  func (target *AMQPTarget) Save(eventData event.Event) error {
   222  	if target.store != nil {
   223  		return target.store.Put(eventData)
   224  	}
   225  	ch, err := target.channel()
   226  	if err != nil {
   227  		return err
   228  	}
   229  	defer func() {
   230  		cErr := ch.Close()
   231  		target.loggerOnce(context.Background(), cErr, target.ID())
   232  	}()
   233  
   234  	return target.send(eventData, ch)
   235  }
   236  
   237  // Send - sends event to AMQP.
   238  func (target *AMQPTarget) Send(eventKey string) error {
   239  	ch, err := target.channel()
   240  	if err != nil {
   241  		return err
   242  	}
   243  	defer func() {
   244  		cErr := ch.Close()
   245  		target.loggerOnce(context.Background(), cErr, target.ID())
   246  	}()
   247  
   248  	eventData, eErr := target.store.Get(eventKey)
   249  	if eErr != nil {
   250  		// The last event key in a successful batch will be sent in the channel atmost once by the replayEvents()
   251  		// Such events will not exist and wouldve been already been sent successfully.
   252  		if os.IsNotExist(eErr) {
   253  			return nil
   254  		}
   255  		return eErr
   256  	}
   257  
   258  	if err := target.send(eventData, ch); err != nil {
   259  		return err
   260  	}
   261  
   262  	// Delete the event from store.
   263  	return target.store.Del(eventKey)
   264  }
   265  
   266  // Close - does nothing and available for interface compatibility.
   267  func (target *AMQPTarget) Close() error {
   268  	if target.conn != nil {
   269  		return target.conn.Close()
   270  	}
   271  	return nil
   272  }
   273  
   274  // NewAMQPTarget - creates new AMQP target.
   275  func NewAMQPTarget(id string, args AMQPArgs, doneCh <-chan struct{}, loggerOnce func(ctx context.Context, err error, id interface{}, errKind ...interface{}), test bool) (*AMQPTarget, error) {
   276  	var conn *amqp.Connection
   277  	var err error
   278  
   279  	var store Store
   280  
   281  	target := &AMQPTarget{
   282  		id:         event.TargetID{ID: id, Name: "amqp"},
   283  		args:       args,
   284  		loggerOnce: loggerOnce,
   285  	}
   286  
   287  	if args.QueueDir != "" {
   288  		queueDir := filepath.Join(args.QueueDir, storePrefix+"-amqp-"+id)
   289  		store = NewQueueStore(queueDir, args.QueueLimit)
   290  		if oErr := store.Open(); oErr != nil {
   291  			target.loggerOnce(context.Background(), oErr, target.ID())
   292  			return target, oErr
   293  		}
   294  		target.store = store
   295  	}
   296  
   297  	conn, err = amqp.Dial(args.URL.String())
   298  	if err != nil {
   299  		if store == nil || !(IsConnRefusedErr(err) || IsConnResetErr(err)) {
   300  			target.loggerOnce(context.Background(), err, target.ID())
   301  			return target, err
   302  		}
   303  	}
   304  	target.conn = conn
   305  
   306  	if target.store != nil && !test {
   307  		// Replays the events from the store.
   308  		eventKeyCh := replayEvents(target.store, doneCh, target.loggerOnce, target.ID())
   309  
   310  		// Start replaying events from the store.
   311  		go sendEvents(target, eventKeyCh, doneCh, target.loggerOnce)
   312  	}
   313  
   314  	return target, nil
   315  }