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 }