github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/event/target/redis.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/url"
    26  	"os"
    27  	"path/filepath"
    28  	"strings"
    29  	"time"
    30  
    31  	"github.com/gomodule/redigo/redis"
    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  // Redis constants
    40  const (
    41  	RedisFormat     = "format"
    42  	RedisAddress    = "address"
    43  	RedisPassword   = "password"
    44  	RedisUser       = "user"
    45  	RedisKey        = "key"
    46  	RedisQueueDir   = "queue_dir"
    47  	RedisQueueLimit = "queue_limit"
    48  
    49  	EnvRedisEnable     = "MINIO_NOTIFY_REDIS_ENABLE"
    50  	EnvRedisFormat     = "MINIO_NOTIFY_REDIS_FORMAT"
    51  	EnvRedisAddress    = "MINIO_NOTIFY_REDIS_ADDRESS"
    52  	EnvRedisPassword   = "MINIO_NOTIFY_REDIS_PASSWORD"
    53  	EnvRedisUser       = "MINIO_NOTIFY_REDIS_USER"
    54  	EnvRedisKey        = "MINIO_NOTIFY_REDIS_KEY"
    55  	EnvRedisQueueDir   = "MINIO_NOTIFY_REDIS_QUEUE_DIR"
    56  	EnvRedisQueueLimit = "MINIO_NOTIFY_REDIS_QUEUE_LIMIT"
    57  )
    58  
    59  // RedisArgs - Redis target arguments.
    60  type RedisArgs struct {
    61  	Enable     bool      `json:"enable"`
    62  	Format     string    `json:"format"`
    63  	Addr       xnet.Host `json:"address"`
    64  	Password   string    `json:"password"`
    65  	User       string    `json:"user"`
    66  	Key        string    `json:"key"`
    67  	QueueDir   string    `json:"queueDir"`
    68  	QueueLimit uint64    `json:"queueLimit"`
    69  }
    70  
    71  // RedisAccessEvent holds event log data and timestamp
    72  type RedisAccessEvent struct {
    73  	Event     []event.Event
    74  	EventTime string
    75  }
    76  
    77  // Validate RedisArgs fields
    78  func (r RedisArgs) Validate() error {
    79  	if !r.Enable {
    80  		return nil
    81  	}
    82  
    83  	if r.Format != "" {
    84  		f := strings.ToLower(r.Format)
    85  		if f != event.NamespaceFormat && f != event.AccessFormat {
    86  			return fmt.Errorf("unrecognized format")
    87  		}
    88  	}
    89  
    90  	if r.Key == "" {
    91  		return fmt.Errorf("empty key")
    92  	}
    93  
    94  	if r.QueueDir != "" {
    95  		if !filepath.IsAbs(r.QueueDir) {
    96  			return errors.New("queueDir path should be absolute")
    97  		}
    98  	}
    99  
   100  	return nil
   101  }
   102  
   103  func (r RedisArgs) validateFormat(c redis.Conn) error {
   104  	typeAvailable, err := redis.String(c.Do("TYPE", r.Key))
   105  	if err != nil {
   106  		return err
   107  	}
   108  
   109  	if typeAvailable != "none" {
   110  		expectedType := "hash"
   111  		if r.Format == event.AccessFormat {
   112  			expectedType = "list"
   113  		}
   114  
   115  		if typeAvailable != expectedType {
   116  			return fmt.Errorf("expected type %v does not match with available type %v", expectedType, typeAvailable)
   117  		}
   118  	}
   119  
   120  	return nil
   121  }
   122  
   123  // RedisTarget - Redis target.
   124  type RedisTarget struct {
   125  	initOnce once.Init
   126  
   127  	id         event.TargetID
   128  	args       RedisArgs
   129  	pool       *redis.Pool
   130  	store      store.Store[event.Event]
   131  	firstPing  bool
   132  	loggerOnce logger.LogOnce
   133  	quitCh     chan struct{}
   134  }
   135  
   136  // ID - returns target ID.
   137  func (target *RedisTarget) ID() event.TargetID {
   138  	return target.id
   139  }
   140  
   141  // Name - returns the Name of the target.
   142  func (target *RedisTarget) Name() string {
   143  	return target.ID().String()
   144  }
   145  
   146  // Store returns any underlying store if set.
   147  func (target *RedisTarget) Store() event.TargetStore {
   148  	return target.store
   149  }
   150  
   151  // IsActive - Return true if target is up and active
   152  func (target *RedisTarget) IsActive() (bool, error) {
   153  	if err := target.init(); err != nil {
   154  		return false, err
   155  	}
   156  	return target.isActive()
   157  }
   158  
   159  func (target *RedisTarget) isActive() (bool, error) {
   160  	conn := target.pool.Get()
   161  	defer conn.Close()
   162  
   163  	_, pingErr := conn.Do("PING")
   164  	if pingErr != nil {
   165  		if xnet.IsConnRefusedErr(pingErr) {
   166  			return false, store.ErrNotConnected
   167  		}
   168  		return false, pingErr
   169  	}
   170  	return true, nil
   171  }
   172  
   173  // Save - saves the events to the store if questore is configured, which will be replayed when the redis connection is active.
   174  func (target *RedisTarget) Save(eventData event.Event) error {
   175  	if target.store != nil {
   176  		return target.store.Put(eventData)
   177  	}
   178  	if err := target.init(); err != nil {
   179  		return err
   180  	}
   181  	_, err := target.isActive()
   182  	if err != nil {
   183  		return err
   184  	}
   185  	return target.send(eventData)
   186  }
   187  
   188  // send - sends an event to the redis.
   189  func (target *RedisTarget) send(eventData event.Event) error {
   190  	conn := target.pool.Get()
   191  	defer conn.Close()
   192  
   193  	if target.args.Format == event.NamespaceFormat {
   194  		objectName, err := url.QueryUnescape(eventData.S3.Object.Key)
   195  		if err != nil {
   196  			return err
   197  		}
   198  		key := eventData.S3.Bucket.Name + "/" + objectName
   199  
   200  		if eventData.EventName == event.ObjectRemovedDelete {
   201  			_, err = conn.Do("HDEL", target.args.Key, key)
   202  		} else {
   203  			var data []byte
   204  			if data, err = json.Marshal(struct{ Records []event.Event }{[]event.Event{eventData}}); err != nil {
   205  				return err
   206  			}
   207  
   208  			_, err = conn.Do("HSET", target.args.Key, key, data)
   209  		}
   210  		if err != nil {
   211  			return err
   212  		}
   213  	}
   214  
   215  	if target.args.Format == event.AccessFormat {
   216  		data, err := json.Marshal([]RedisAccessEvent{{Event: []event.Event{eventData}, EventTime: eventData.EventTime}})
   217  		if err != nil {
   218  			return err
   219  		}
   220  		if _, err := conn.Do("RPUSH", target.args.Key, data); err != nil {
   221  			return err
   222  		}
   223  	}
   224  
   225  	return nil
   226  }
   227  
   228  // SendFromStore - reads an event from store and sends it to redis.
   229  func (target *RedisTarget) SendFromStore(key store.Key) error {
   230  	if err := target.init(); err != nil {
   231  		return err
   232  	}
   233  
   234  	conn := target.pool.Get()
   235  	defer conn.Close()
   236  
   237  	_, pingErr := conn.Do("PING")
   238  	if pingErr != nil {
   239  		if xnet.IsConnRefusedErr(pingErr) {
   240  			return store.ErrNotConnected
   241  		}
   242  		return pingErr
   243  	}
   244  
   245  	if !target.firstPing {
   246  		if err := target.args.validateFormat(conn); err != nil {
   247  			if xnet.IsConnRefusedErr(err) {
   248  				return store.ErrNotConnected
   249  			}
   250  			return err
   251  		}
   252  		target.firstPing = true
   253  	}
   254  
   255  	eventData, eErr := target.store.Get(key.Name)
   256  	if eErr != nil {
   257  		// The last event key in a successful batch will be sent in the channel atmost once by the replayEvents()
   258  		// Such events will not exist and would've been already been sent successfully.
   259  		if os.IsNotExist(eErr) {
   260  			return nil
   261  		}
   262  		return eErr
   263  	}
   264  
   265  	if err := target.send(eventData); err != nil {
   266  		if xnet.IsConnRefusedErr(err) {
   267  			return store.ErrNotConnected
   268  		}
   269  		return err
   270  	}
   271  
   272  	// Delete the event from store.
   273  	return target.store.Del(key.Name)
   274  }
   275  
   276  // Close - releases the resources used by the pool.
   277  func (target *RedisTarget) Close() error {
   278  	close(target.quitCh)
   279  	if target.pool != nil {
   280  		return target.pool.Close()
   281  	}
   282  	return nil
   283  }
   284  
   285  func (target *RedisTarget) init() error {
   286  	return target.initOnce.Do(target.initRedis)
   287  }
   288  
   289  func (target *RedisTarget) initRedis() error {
   290  	conn := target.pool.Get()
   291  	defer conn.Close()
   292  
   293  	_, pingErr := conn.Do("PING")
   294  	if pingErr != nil {
   295  		if !(xnet.IsConnRefusedErr(pingErr) || xnet.IsConnResetErr(pingErr)) {
   296  			target.loggerOnce(context.Background(), pingErr, target.ID().String())
   297  		}
   298  		return pingErr
   299  	}
   300  
   301  	if err := target.args.validateFormat(conn); err != nil {
   302  		target.loggerOnce(context.Background(), err, target.ID().String())
   303  		return err
   304  	}
   305  
   306  	target.firstPing = true
   307  
   308  	yes, err := target.isActive()
   309  	if err != nil {
   310  		return err
   311  	}
   312  	if !yes {
   313  		return store.ErrNotConnected
   314  	}
   315  
   316  	return nil
   317  }
   318  
   319  // NewRedisTarget - creates new Redis target.
   320  func NewRedisTarget(id string, args RedisArgs, loggerOnce logger.LogOnce) (*RedisTarget, error) {
   321  	var queueStore store.Store[event.Event]
   322  	if args.QueueDir != "" {
   323  		queueDir := filepath.Join(args.QueueDir, storePrefix+"-redis-"+id)
   324  		queueStore = store.NewQueueStore[event.Event](queueDir, args.QueueLimit, event.StoreExtension)
   325  		if err := queueStore.Open(); err != nil {
   326  			return nil, fmt.Errorf("unable to initialize the queue store of Redis `%s`: %w", id, err)
   327  		}
   328  	}
   329  
   330  	pool := &redis.Pool{
   331  		MaxIdle:     3,
   332  		IdleTimeout: 2 * 60 * time.Second,
   333  		Dial: func() (redis.Conn, error) {
   334  			conn, err := redis.Dial("tcp", args.Addr.String())
   335  			if err != nil {
   336  				return nil, err
   337  			}
   338  
   339  			if args.Password != "" {
   340  				if args.User != "" {
   341  					if _, err = conn.Do("AUTH", args.User, args.Password); err != nil {
   342  						conn.Close()
   343  						return nil, err
   344  					}
   345  				} else {
   346  					if _, err = conn.Do("AUTH", args.Password); err != nil {
   347  						conn.Close()
   348  						return nil, err
   349  					}
   350  				}
   351  			}
   352  
   353  			// Must be done after AUTH
   354  			if _, err = conn.Do("CLIENT", "SETNAME", "MinIO"); err != nil {
   355  				conn.Close()
   356  				return nil, err
   357  			}
   358  
   359  			return conn, nil
   360  		},
   361  		TestOnBorrow: func(c redis.Conn, t time.Time) error {
   362  			_, err := c.Do("PING")
   363  			return err
   364  		},
   365  	}
   366  
   367  	target := &RedisTarget{
   368  		id:         event.TargetID{ID: id, Name: "redis"},
   369  		args:       args,
   370  		pool:       pool,
   371  		store:      queueStore,
   372  		loggerOnce: loggerOnce,
   373  		quitCh:     make(chan struct{}),
   374  	}
   375  
   376  	if target.store != nil {
   377  		store.StreamItems(target.store, target, target.quitCh, target.loggerOnce)
   378  	}
   379  
   380  	return target, nil
   381  }