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