github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/event/target/nsq.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  	"crypto/tls"
    23  	"encoding/json"
    24  	"errors"
    25  	"fmt"
    26  	"net/url"
    27  	"os"
    28  	"path/filepath"
    29  
    30  	"github.com/nsqio/go-nsq"
    31  
    32  	"github.com/minio/minio/internal/event"
    33  	"github.com/minio/minio/internal/logger"
    34  	"github.com/minio/minio/internal/once"
    35  	"github.com/minio/minio/internal/store"
    36  	xnet "github.com/minio/pkg/v2/net"
    37  )
    38  
    39  // NSQ constants
    40  const (
    41  	NSQAddress       = "nsqd_address"
    42  	NSQTopic         = "topic"
    43  	NSQTLS           = "tls"
    44  	NSQTLSSkipVerify = "tls_skip_verify"
    45  	NSQQueueDir      = "queue_dir"
    46  	NSQQueueLimit    = "queue_limit"
    47  
    48  	EnvNSQEnable        = "MINIO_NOTIFY_NSQ_ENABLE"
    49  	EnvNSQAddress       = "MINIO_NOTIFY_NSQ_NSQD_ADDRESS"
    50  	EnvNSQTopic         = "MINIO_NOTIFY_NSQ_TOPIC"
    51  	EnvNSQTLS           = "MINIO_NOTIFY_NSQ_TLS"
    52  	EnvNSQTLSSkipVerify = "MINIO_NOTIFY_NSQ_TLS_SKIP_VERIFY"
    53  	EnvNSQQueueDir      = "MINIO_NOTIFY_NSQ_QUEUE_DIR"
    54  	EnvNSQQueueLimit    = "MINIO_NOTIFY_NSQ_QUEUE_LIMIT"
    55  )
    56  
    57  // NSQArgs - NSQ target arguments.
    58  type NSQArgs struct {
    59  	Enable      bool      `json:"enable"`
    60  	NSQDAddress xnet.Host `json:"nsqdAddress"`
    61  	Topic       string    `json:"topic"`
    62  	TLS         struct {
    63  		Enable     bool `json:"enable"`
    64  		SkipVerify bool `json:"skipVerify"`
    65  	} `json:"tls"`
    66  	QueueDir   string `json:"queueDir"`
    67  	QueueLimit uint64 `json:"queueLimit"`
    68  }
    69  
    70  // Validate NSQArgs fields
    71  func (n NSQArgs) Validate() error {
    72  	if !n.Enable {
    73  		return nil
    74  	}
    75  
    76  	if n.NSQDAddress.IsEmpty() {
    77  		return errors.New("empty nsqdAddress")
    78  	}
    79  
    80  	if n.Topic == "" {
    81  		return errors.New("empty topic")
    82  	}
    83  	if n.QueueDir != "" {
    84  		if !filepath.IsAbs(n.QueueDir) {
    85  			return errors.New("queueDir path should be absolute")
    86  		}
    87  	}
    88  
    89  	return nil
    90  }
    91  
    92  // NSQTarget - NSQ target.
    93  type NSQTarget struct {
    94  	initOnce once.Init
    95  
    96  	id         event.TargetID
    97  	args       NSQArgs
    98  	producer   *nsq.Producer
    99  	store      store.Store[event.Event]
   100  	config     *nsq.Config
   101  	loggerOnce logger.LogOnce
   102  	quitCh     chan struct{}
   103  }
   104  
   105  // ID - returns target ID.
   106  func (target *NSQTarget) ID() event.TargetID {
   107  	return target.id
   108  }
   109  
   110  // Name - returns the Name of the target.
   111  func (target *NSQTarget) Name() string {
   112  	return target.ID().String()
   113  }
   114  
   115  // Store returns any underlying store if set.
   116  func (target *NSQTarget) Store() event.TargetStore {
   117  	return target.store
   118  }
   119  
   120  // IsActive - Return true if target is up and active
   121  func (target *NSQTarget) IsActive() (bool, error) {
   122  	if err := target.init(); err != nil {
   123  		return false, err
   124  	}
   125  	return target.isActive()
   126  }
   127  
   128  func (target *NSQTarget) isActive() (bool, error) {
   129  	if target.producer == nil {
   130  		producer, err := nsq.NewProducer(target.args.NSQDAddress.String(), target.config)
   131  		if err != nil {
   132  			return false, err
   133  		}
   134  		target.producer = producer
   135  	}
   136  
   137  	if err := target.producer.Ping(); err != nil {
   138  		// To treat "connection refused" errors as errNotConnected.
   139  		if xnet.IsConnRefusedErr(err) {
   140  			return false, store.ErrNotConnected
   141  		}
   142  		return false, err
   143  	}
   144  	return true, nil
   145  }
   146  
   147  // Save - saves the events to the store which will be replayed when the nsq connection is active.
   148  func (target *NSQTarget) Save(eventData event.Event) error {
   149  	if target.store != nil {
   150  		return target.store.Put(eventData)
   151  	}
   152  
   153  	if err := target.init(); err != nil {
   154  		return err
   155  	}
   156  
   157  	_, err := target.isActive()
   158  	if err != nil {
   159  		return err
   160  	}
   161  	return target.send(eventData)
   162  }
   163  
   164  // send - sends an event to the NSQ.
   165  func (target *NSQTarget) send(eventData event.Event) error {
   166  	objectName, err := url.QueryUnescape(eventData.S3.Object.Key)
   167  	if err != nil {
   168  		return err
   169  	}
   170  	key := eventData.S3.Bucket.Name + "/" + objectName
   171  
   172  	data, err := json.Marshal(event.Log{EventName: eventData.EventName, Key: key, Records: []event.Event{eventData}})
   173  	if err != nil {
   174  		return err
   175  	}
   176  
   177  	return target.producer.Publish(target.args.Topic, data)
   178  }
   179  
   180  // SendFromStore - reads an event from store and sends it to NSQ.
   181  func (target *NSQTarget) SendFromStore(key store.Key) error {
   182  	if err := target.init(); err != nil {
   183  		return err
   184  	}
   185  
   186  	_, err := target.isActive()
   187  	if err != nil {
   188  		return err
   189  	}
   190  
   191  	eventData, eErr := target.store.Get(key.Name)
   192  	if eErr != nil {
   193  		// The last event key in a successful batch will be sent in the channel atmost once by the replayEvents()
   194  		// Such events will not exist and wouldve been already been sent successfully.
   195  		if os.IsNotExist(eErr) {
   196  			return nil
   197  		}
   198  		return eErr
   199  	}
   200  
   201  	if err := target.send(eventData); err != nil {
   202  		return err
   203  	}
   204  
   205  	// Delete the event from store.
   206  	return target.store.Del(key.Name)
   207  }
   208  
   209  // Close - closes underneath connections to NSQD server.
   210  func (target *NSQTarget) Close() (err error) {
   211  	close(target.quitCh)
   212  	if target.producer != nil {
   213  		// this blocks until complete:
   214  		target.producer.Stop()
   215  	}
   216  	return nil
   217  }
   218  
   219  func (target *NSQTarget) init() error {
   220  	return target.initOnce.Do(target.initNSQ)
   221  }
   222  
   223  func (target *NSQTarget) initNSQ() error {
   224  	args := target.args
   225  
   226  	config := nsq.NewConfig()
   227  	if args.TLS.Enable {
   228  		config.TlsV1 = true
   229  		config.TlsConfig = &tls.Config{
   230  			InsecureSkipVerify: args.TLS.SkipVerify,
   231  		}
   232  	}
   233  	target.config = config
   234  
   235  	producer, err := nsq.NewProducer(args.NSQDAddress.String(), config)
   236  	if err != nil {
   237  		target.loggerOnce(context.Background(), err, target.ID().String())
   238  		return err
   239  	}
   240  	target.producer = producer
   241  
   242  	err = target.producer.Ping()
   243  	if err != nil {
   244  		// To treat "connection refused" errors as errNotConnected.
   245  		if !(xnet.IsConnRefusedErr(err) || xnet.IsConnResetErr(err)) {
   246  			target.loggerOnce(context.Background(), err, target.ID().String())
   247  		}
   248  		target.producer.Stop()
   249  		return err
   250  	}
   251  
   252  	yes, err := target.isActive()
   253  	if err != nil {
   254  		return err
   255  	}
   256  	if !yes {
   257  		return store.ErrNotConnected
   258  	}
   259  
   260  	return nil
   261  }
   262  
   263  // NewNSQTarget - creates new NSQ target.
   264  func NewNSQTarget(id string, args NSQArgs, loggerOnce logger.LogOnce) (*NSQTarget, error) {
   265  	var queueStore store.Store[event.Event]
   266  	if args.QueueDir != "" {
   267  		queueDir := filepath.Join(args.QueueDir, storePrefix+"-nsq-"+id)
   268  		queueStore = store.NewQueueStore[event.Event](queueDir, args.QueueLimit, event.StoreExtension)
   269  		if err := queueStore.Open(); err != nil {
   270  			return nil, fmt.Errorf("unable to initialize the queue store of NSQ `%s`: %w", id, err)
   271  		}
   272  	}
   273  
   274  	target := &NSQTarget{
   275  		id:         event.TargetID{ID: id, Name: "nsq"},
   276  		args:       args,
   277  		loggerOnce: loggerOnce,
   278  		store:      queueStore,
   279  		quitCh:     make(chan struct{}),
   280  	}
   281  
   282  	if target.store != nil {
   283  		store.StreamItems(target.store, target, target.quitCh, target.loggerOnce)
   284  	}
   285  
   286  	return target, nil
   287  }