github.com/projecteru2/core@v0.0.0-20240321043226-06bcc1c23f58/store/redis/rediaron.go (about)

     1  package redis
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/panjf2000/ants/v2"
    11  	"github.com/projecteru2/core/log"
    12  	"github.com/projecteru2/core/types"
    13  	"github.com/projecteru2/core/utils"
    14  
    15  	"github.com/cockroachdb/errors"
    16  	"github.com/go-redis/redis/v8"
    17  )
    18  
    19  var (
    20  	// ErrMaxRetryExceeded indicates redis transaction failed after all the retries
    21  	ErrMaxRetryExceeded = errors.New("[Redis transaction] Max retry exceeded")
    22  	// ErrAlreadyExists indicates the key already exists when do redis SETNX
    23  	ErrAlreadyExists = errors.New("[Redis setnx] Already exists")
    24  	// ErrBadCmdType indicates command type is not correct
    25  	// e.g. SET should be StringCmd
    26  	ErrBadCmdType = errors.New("[Redis cmd] Bad cmd type")
    27  	// ErrKeyNotExitsts indicates no key found
    28  	// When do update, we need to ensure the key exists, just like the behavior of etcd client
    29  	ErrKeyNotExitsts = errors.New("[Redis exists] Key not exists")
    30  )
    31  
    32  const (
    33  	// storage key pattern
    34  	podInfoKey       = "/pod/info/%s" // /pod/info/{podname}
    35  	serviceStatusKey = "/services/%s" // /service/{ipv4:port}
    36  
    37  	nodeInfoKey      = "/node/%s"              // /node/{nodename}
    38  	nodePodKey       = "/node/%s:pod/%s"       // /node/{podname}:pod/{nodename}
    39  	nodeCaKey        = "/node/%s:ca"           // /node/{nodename}:ca
    40  	nodeCertKey      = "/node/%s:cert"         // /node/{nodename}:cert
    41  	nodeKeyKey       = "/node/%s:key"          // /node/{nodename}:key
    42  	nodeStatusPrefix = "/status:node/"         // /status:node/{nodename} -> node status key
    43  	nodeWorkloadsKey = "/node/%s:workloads/%s" // /node/{nodename}:workloads/{workloadID}
    44  
    45  	workloadInfoKey          = "/workloads/%s" // /workloads/{workloadID}
    46  	workloadDeployPrefix     = "/deploy"       // /deploy/{appname}/{entrypoint}/{nodename}/{workloadID}
    47  	workloadStatusPrefix     = "/status"       // /status/{appname}/{entrypoint}/{nodename}/{workloadID} value -> something by agent
    48  	workloadProcessingPrefix = "/processing"   // /processing/{appname}/{entrypoint}/{nodename}/{opsIdent} value -> count
    49  
    50  	// keyspace notification prefix pattern
    51  	keyNotifyPrefix = "__keyspace@%d__:%s"
    52  
    53  	// key event action
    54  	actionExpire  = "expire"
    55  	actionExpired = "expired"
    56  	actionSet     = "set"
    57  	actionDel     = "del"
    58  )
    59  
    60  // Rediaron is a store implemented by redis
    61  type Rediaron struct {
    62  	cli    *redis.Client
    63  	config types.Config
    64  	pool   *ants.PoolWithFunc
    65  	db     int
    66  }
    67  
    68  // New creates a new Rediaron instance from config
    69  // Only redis address and db is used
    70  // db is used to separate data, by default db 0 will be used
    71  // TODO mock redis for testing
    72  func New(config types.Config, _ *testing.T) (*Rediaron, error) {
    73  	cli := redis.NewClient(&redis.Options{
    74  		Addr: config.Redis.Addr,
    75  		DB:   config.Redis.DB,
    76  	})
    77  	pool, err := utils.NewPool(config.MaxConcurrency)
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  	return &Rediaron{
    82  		cli:    cli,
    83  		config: config,
    84  		pool:   pool,
    85  		db:     config.Redis.DB,
    86  	}, nil
    87  }
    88  
    89  // KNotifyMessage is received when using KNotify
    90  type KNotifyMessage struct {
    91  	Key    string
    92  	Action string
    93  }
    94  
    95  // KNotify is like `watch` in etcd
    96  // knotify comes from inotify, when a key is changed, notification will be published
    97  func (r *Rediaron) KNotify(ctx context.Context, pattern string) chan *KNotifyMessage {
    98  	ch := make(chan *KNotifyMessage)
    99  	_ = r.pool.Invoke(func() {
   100  		defer close(ch)
   101  
   102  		prefix := fmt.Sprintf(keyNotifyPrefix, r.db, "")
   103  		channel := fmt.Sprintf(keyNotifyPrefix, r.db, pattern)
   104  		pubsub := r.cli.PSubscribe(ctx, channel)
   105  		subC := pubsub.Channel()
   106  
   107  		for {
   108  			select {
   109  			case <-ctx.Done():
   110  				pubsub.Close()
   111  				return
   112  			case v := <-subC:
   113  				if v == nil {
   114  					log.WithFunc("store.redis.KNotify").Warnf(ctx, "channel already closed, knotify returns")
   115  					return
   116  				}
   117  				ch <- &KNotifyMessage{
   118  					Key:    strings.TrimPrefix(v.Channel, prefix),
   119  					Action: strings.ToLower(v.Payload),
   120  				}
   121  			}
   122  		}
   123  	})
   124  	return ch
   125  }
   126  
   127  // GetOne is a wrapper
   128  func (r *Rediaron) GetOne(ctx context.Context, key string) (string, error) {
   129  	value, err := r.cli.Get(ctx, key).Result()
   130  	if isRedisNoKeyError(err) {
   131  		return "", errors.Wrapf(err, "Key not found: %s", key)
   132  	}
   133  	return value, err
   134  }
   135  
   136  // GetMulti is a wrapper
   137  func (r *Rediaron) GetMulti(ctx context.Context, keys []string) (map[string]string, error) {
   138  	data := map[string]string{}
   139  	fetch := func(pipe redis.Pipeliner) error {
   140  		for _, k := range keys {
   141  			_, err := pipe.Get(ctx, k).Result()
   142  			if err != nil {
   143  				return err
   144  			}
   145  		}
   146  		return nil
   147  	}
   148  	cmders, err := r.cli.Pipelined(ctx, fetch)
   149  	for _, cmd := range cmders {
   150  		c, ok := cmd.(*redis.StringCmd)
   151  		if !ok {
   152  			return nil, ErrBadCmdType
   153  		}
   154  
   155  		args := c.Args()
   156  		if len(args) != 2 {
   157  			return nil, ErrBadCmdType
   158  		}
   159  
   160  		key, ok := args[1].(string)
   161  		if !ok {
   162  			return nil, ErrBadCmdType
   163  		}
   164  
   165  		if isRedisNoKeyError(c.Err()) {
   166  			return nil, errors.Wrapf(err, "Key not found: %s", key)
   167  		}
   168  
   169  		data[key] = c.Val()
   170  	}
   171  	return data, err
   172  }
   173  
   174  // BatchUpdate is wrapper to adapt etcd batch update
   175  func (r *Rediaron) BatchUpdate(ctx context.Context, data map[string]string) error {
   176  	keys := []string{}
   177  	for k := range data {
   178  		keys = append(keys, k)
   179  	}
   180  
   181  	// check existence of keys
   182  	// FIXME: no transaction ensured
   183  	e, err := r.cli.Exists(ctx, keys...).Result()
   184  	if err != nil {
   185  		return err
   186  	}
   187  	if int(e) != len(keys) {
   188  		return ErrKeyNotExitsts
   189  	}
   190  
   191  	update := func(pipe redis.Pipeliner) error {
   192  		for key, value := range data {
   193  			pipe.Set(ctx, key, value, 0)
   194  		}
   195  		return nil
   196  	}
   197  
   198  	cmds, err := r.cli.TxPipelined(ctx, update)
   199  	if err != nil {
   200  		return err
   201  	}
   202  
   203  	for _, cmd := range cmds {
   204  		if err := cmd.Err(); err != nil {
   205  			return err
   206  		}
   207  	}
   208  	return nil
   209  }
   210  
   211  // BatchCreate is wrapper to adapt etcd batch create
   212  func (r *Rediaron) BatchCreate(ctx context.Context, data map[string]string) error {
   213  	create := func(pipe redis.Pipeliner) error {
   214  		for key, value := range data {
   215  			pipe.SetNX(ctx, key, value, 0)
   216  		}
   217  		return nil
   218  	}
   219  
   220  	cmds, err := r.cli.TxPipelined(ctx, create)
   221  	if err != nil {
   222  		return err
   223  	}
   224  
   225  	for _, cmd := range cmds {
   226  		bc, ok := cmd.(*redis.BoolCmd)
   227  		if !ok {
   228  			return ErrBadCmdType
   229  		}
   230  
   231  		created, err := bc.Result()
   232  		if !created {
   233  			return ErrAlreadyExists
   234  		}
   235  		if err != nil {
   236  			return err
   237  		}
   238  	}
   239  	return nil
   240  }
   241  
   242  // BatchPut is wrapper to adapt etcd batch replace
   243  func (r *Rediaron) BatchPut(ctx context.Context, data map[string]string) error {
   244  	replace := func(pipe redis.Pipeliner) error {
   245  		for key, value := range data {
   246  			pipe.Set(ctx, key, value, 0)
   247  		}
   248  		return nil
   249  	}
   250  
   251  	cmds, err := r.cli.TxPipelined(ctx, replace)
   252  	if err != nil {
   253  		return err
   254  	}
   255  
   256  	for _, cmd := range cmds {
   257  		if err := cmd.Err(); err != nil {
   258  			return err
   259  		}
   260  	}
   261  	return nil
   262  }
   263  
   264  // BatchCreateAndDecr decr processing and add workload
   265  func (r *Rediaron) BatchCreateAndDecr(ctx context.Context, data map[string]string, decrKey string) (err error) {
   266  	batchCreateAndDecr := func(pipe redis.Pipeliner) error {
   267  		pipe.Decr(ctx, decrKey)
   268  		for key, value := range data {
   269  			pipe.SetNX(ctx, key, value, 0)
   270  		}
   271  		return nil
   272  	}
   273  	_, err = r.cli.TxPipelined(ctx, batchCreateAndDecr)
   274  	return
   275  }
   276  
   277  // BatchDelete is wrapper to adapt etcd batch delete
   278  func (r *Rediaron) BatchDelete(ctx context.Context, keys []string) error {
   279  	del := func(pipe redis.Pipeliner) error {
   280  		for _, key := range keys {
   281  			pipe.Del(ctx, key)
   282  		}
   283  		return nil
   284  	}
   285  	_, err := r.cli.TxPipelined(ctx, del)
   286  	return err
   287  }
   288  
   289  // BindStatus is wrapper to adapt etcd bind status
   290  func (r *Rediaron) BindStatus(ctx context.Context, entityKey, statusKey, statusValue string, ttl int64) error {
   291  	count, err := r.cli.Exists(ctx, entityKey).Result()
   292  	if err != nil {
   293  		return err
   294  	}
   295  	// doesn't exist, returns error
   296  	// to behave just like etcd
   297  	if count != 1 {
   298  		return types.ErrInvaildCount
   299  	}
   300  
   301  	_, err = r.cli.Set(ctx, statusKey, statusValue, time.Duration(ttl)*time.Second).Result()
   302  	return err
   303  }
   304  
   305  // TerminateEmbededStorage terminates embedded store
   306  // in order to implement Store interface
   307  // we can't use embedded redis, it doesn't support keyspace notification
   308  // never call this except running unittests
   309  func (r *Rediaron) TerminateEmbededStorage() {
   310  	_ = r.cli.Close()
   311  }
   312  
   313  // go-redis doesn't export its proto.Error type,
   314  // we have to check the content in this error
   315  func isRedisNoKeyError(e error) bool {
   316  	return e != nil && strings.Contains(e.Error(), "redis: nil")
   317  }