github.com/influx6/npkg@v0.8.8/nstorage/nredis/nredis.go (about) 1 package nredis 2 3 import ( 4 "context" 5 regexp2 "regexp" 6 "strings" 7 "time" 8 9 redis "github.com/go-redis/redis/v8" 10 11 "github.com/influx6/npkg/nerror" 12 "github.com/influx6/npkg/nstorage" 13 "github.com/influx6/npkg/nunsafe" 14 ) 15 16 var _ nstorage.ExpirableStore = (*RedisStore)(nil) 17 18 // RedisStore implements session management, storage and access using redis as 19 // underline store. 20 type RedisStore struct { 21 ctx context.Context 22 tableName string 23 hashList string 24 hashZList string 25 hashElem string 26 Config *redis.Options 27 Client *redis.Client 28 } 29 30 // NewRedisStore returns a new instance of a redis store. 31 func NewRedisStore(ctx context.Context, tableName string, config redis.Options) (*RedisStore, error) { 32 var red RedisStore 33 red.ctx = ctx 34 red.tableName = tableName 35 red.hashList = tableName + "_keys" 36 red.hashElem = tableName + "_item" 37 red.Config = &config 38 if err := red.createConnection(); err != nil { 39 return nil, nerror.WrapOnly(err) 40 } 41 return &red, nil 42 } 43 44 // FromRedisStore returns a new instance of a RedisStore using giving client. 45 func FromRedisStore(ctx context.Context, tableName string, conn *redis.Client) (*RedisStore, error) { 46 if status := conn.Ping(ctx); status.Err() != nil { 47 return nil, status.Err() 48 } 49 50 var red RedisStore 51 red.ctx = ctx 52 red.tableName = tableName 53 red.hashList = tableName + "_keys" 54 red.hashElem = tableName + "_item" 55 red.hashZList = tableName + "_zset" 56 red.Client = conn 57 return &red, nil 58 } 59 60 // createConnection attempts to create a new redis connection. 61 func (rd *RedisStore) createConnection() error { 62 client := redis.NewClient(rd.Config) 63 status := client.Ping(rd.ctx) 64 if err := status.Err(); err != nil { 65 return nerror.WrapOnly(err) 66 } 67 rd.Client = client 68 return nil 69 } 70 71 func (rd *RedisStore) Close() error { 72 return rd.Client.Close() 73 } 74 75 // doHashKey returns formatted for unique form towards using creating 76 // efficient hashmaps to contain list of keys. 77 func (rd *RedisStore) doHashKey(key string) string { 78 return strings.Join([]string{rd.hashElem, key}, "_") 79 } 80 81 func (rd *RedisStore) unHashKey(key string) string { 82 return strings.TrimPrefix(key, rd.hashElem+"_") 83 } 84 85 func (rd *RedisStore) unHashKeyList(keys []string) []string { 86 for index, key := range keys { 87 keys[index] = rd.unHashKey(key) 88 } 89 return keys 90 } 91 92 // Keys returns all giving keys of elements within store. 93 func (rd *RedisStore) Keys() ([]string, error) { 94 var nstatus = rd.Client.SMembers(rd.ctx, rd.hashList) 95 if err := nstatus.Err(); err != nil { 96 return nil, nerror.WrapOnly(err) 97 } 98 99 return rd.unHashKeyList(nstatus.Val()), nil 100 } 101 102 // Each runs through all elements for giving store, skipping keys 103 // in redis who have no data or an empty byte slice. 104 func (rd *RedisStore) Each(fn nstorage.EachItem) error { 105 var nstatus = rd.Client.SMembers(rd.ctx, rd.hashList) 106 if err := nstatus.Err(); err != nil { 107 return nerror.WrapOnly(err) 108 } 109 110 var keys = nstatus.Val() 111 var pipeliner = rd.Client.Pipeline() 112 113 var values = make([]*redis.StringCmd, len(keys)) 114 for index, key := range keys { 115 var result = pipeliner.Get(rd.ctx, key) 116 values[index] = result 117 } 118 119 var _, err = pipeliner.Exec(rd.ctx) 120 if err != nil && err != redis.Nil { 121 return nerror.WrapOnly(err) 122 } 123 124 for index, item := range values { 125 if item.Err() != nil { 126 continue 127 } 128 var key = keys[index] 129 var data = nunsafe.String2Bytes(item.Val()) 130 if doErr := fn(data, key); doErr != nil { 131 if nerror.IsAny(doErr, nstorage.ErrJustStop) { 132 return nil 133 } 134 return doErr 135 } 136 } 137 return nil 138 } 139 140 // EachKeyPrefix returns all matching values within store, if elements found match giving 141 // count then all values returned. 142 // 143 // if an error occurs, the partially collected list of keys and error is returned. 144 // 145 // Return nstorage.ErrJustStop if you want to just stop iterating. 146 func (rd *RedisStore) EachKeyMatch(regexp string) ([]string, error) { 147 return rd.FindPrefixFor(100, regexp) 148 } 149 150 // ScanMatche uses underline redis scan methods for a hashmap, relying on the lastIndex 151 // as a way to track the last cursor point on the store. Note that due to the way redis works 152 // works, the count is not guaranteed to stay as such, it can be ignored and more may be returned 153 // or less/none. 154 // 155 // With scan the order is not guaranteed. 156 func (rd *RedisStore) ScanMatch(count int64, lastIndex int64, _ string, regexp string) (nstorage.ScanResult, error) { 157 if len(regexp) == 0 { 158 regexp = ".+" 159 } 160 161 var rs nstorage.ScanResult 162 var regx, rgErr = regexp2.Compile(regexp) 163 if rgErr != nil { 164 return rs, nerror.WrapOnly(rgErr) 165 } 166 167 var scanned = rd.Client.ZRange(rd.ctx, rd.hashZList, lastIndex, lastIndex+count-1) 168 var ky, err = scanned.Result() 169 if err != nil { 170 return rs, nerror.WrapOnly(err) 171 } 172 173 var keys = make([]string, 0, len(ky)) 174 for _, item := range ky { 175 var ritem = rd.unHashKey(item) 176 if !regx.MatchString(ritem) { 177 continue 178 } 179 keys = append(keys, ritem) 180 } 181 182 // rs.Finished = cursor == 0 183 var lastKey string 184 if keysCount := len(keys); keysCount > 0 { 185 lastKey = keys[count-1] 186 } 187 188 // var isFinished = cursor == 0 189 return nstorage.ScanResult{ 190 Finished: false, 191 Keys: keys, 192 LastIndex: lastIndex + count, 193 LastKey: lastKey, 194 }, nil 195 } 196 197 // Count returns the total count of element in the store. 198 func (rd *RedisStore) Count() (int64, error) { 199 var command = rd.Client.HLen(rd.ctx, rd.hashElem) 200 201 var err = command.Err() 202 if err != nil { 203 return -1, nerror.WrapOnly(err) 204 } 205 206 var count = command.Val() 207 return count, nil 208 } 209 210 // FindPrefixFor returns all matching values within store, if elements found match giving 211 // count then all values returned. 212 // 213 // if an error occurs, the partially collected list of keys and error is returned. 214 func (rd *RedisStore) FindPrefixFor(count int64, regexp string) ([]string, error) { 215 if len(regexp) == 0 { 216 regexp = ".+" 217 } 218 219 var regx, rgErr = regexp2.Compile(regexp) 220 if rgErr != nil { 221 return nil, nerror.WrapOnly(rgErr) 222 } 223 224 var cursor uint64 225 var keys = make([]string, 0, count) 226 var err error 227 for { 228 var scanned = rd.Client.SScan(rd.ctx, rd.hashList, cursor, "*", count) 229 var ky, cursor, err = scanned.Result() 230 if err != nil { 231 return keys, nerror.WrapOnly(err) 232 } 233 234 for _, item := range ky { 235 if !regx.MatchString(item) { 236 continue 237 } 238 keys = append(keys, item) 239 } 240 241 if cursor == 0 { 242 break 243 } 244 } 245 246 if err != nil { 247 return nil, nerror.WrapOnly(err) 248 } 249 return keys, nil 250 } 251 252 // Exists returns true/false if giving key exists. 253 func (rd *RedisStore) Exists(key string) (bool, error) { 254 var hashKey = rd.doHashKey(key) 255 var nstatus = rd.Client.SIsMember(rd.ctx, rd.hashList, hashKey) 256 if err := nstatus.Err(); err != nil { 257 return false, nerror.WrapOnly(err) 258 } 259 return nstatus.Val(), nil 260 } 261 262 // exists returns true/false if giving key is set in redis. 263 func (rd *RedisStore) exists(key string) (bool, error) { 264 var hashKey = rd.doHashKey(key) 265 var nstatus = rd.Client.Exists(rd.ctx, hashKey) 266 if err := nstatus.Err(); err != nil { 267 return false, nerror.WrapOnly(err) 268 } 269 return nstatus.Val() == 1, nil 270 } 271 272 // expire expires giving key set from underline hash set. 273 func (rd *RedisStore) expire(keys []string) error { 274 var items = make([]interface{}, len(keys)) 275 for index, elem := range keys { 276 items[index] = elem 277 } 278 var _, err = rd.Client.TxPipelined(rd.ctx, func(pipeliner redis.Pipeliner) error { 279 var zstatus = pipeliner.ZRem(rd.ctx, rd.hashZList, items...) 280 if err := zstatus.Err(); err != nil { 281 return err 282 } 283 var mstatus = pipeliner.SRem(rd.ctx, rd.hashList, items...) 284 if err := mstatus.Err(); err != nil { 285 return nerror.WrapOnly(err) 286 } 287 var dstatus = pipeliner.Del(rd.ctx, keys...) 288 if err := dstatus.Err(); err != nil { 289 return nerror.WrapOnly(err) 290 } 291 return nil 292 }) 293 if err != nil { 294 return nerror.WrapOnly(err) 295 } 296 return nil 297 } 298 299 // Save adds giving session into storage using redis as underline store. 300 func (rd *RedisStore) Save(key string, data []byte) error { 301 return rd.SaveTTL(key, data, 0) 302 } 303 304 // SaveTTL adds giving session into storage using redis as underline store, with provided 305 // expiration. 306 // Duration of 0 means no expiration. 307 func (rd *RedisStore) SaveTTL(key string, data []byte, expiration time.Duration) error { 308 var hashKey = rd.doHashKey(key) 309 var _, pipeErr = rd.Client.TxPipelined(rd.ctx, func(pipeliner redis.Pipeliner) error { 310 var nstatus = pipeliner.SAdd(rd.ctx, rd.hashList, hashKey) 311 if err := nstatus.Err(); err != nil { 312 return nerror.WrapOnly(err) 313 } 314 315 var zs redis.Z 316 zs.Score = 0 317 zs.Member = hashKey 318 319 var zstatus = pipeliner.ZAdd(rd.ctx, rd.hashZList, &zs) 320 if err := zstatus.Err(); err != nil { 321 return nerror.WrapOnly(err) 322 } 323 324 var nset = pipeliner.Set(rd.ctx, hashKey, data, expiration) 325 if err := nset.Err(); err != nil { 326 return nerror.WrapOnly(err) 327 } 328 return nil 329 }) 330 331 if err := pipeErr; err != nil { 332 return nerror.WrapOnly(pipeErr) 333 } 334 335 return nil 336 } 337 338 // Update updates giving key with new data slice with 0 duration. 339 func (rd *RedisStore) Update(key string, data []byte) error { 340 return rd.UpdateTTL(key, data, 0) 341 } 342 343 // UpdateTTL updates giving session stored with giving key. It updates 344 // the underline data and increases the expiration with provided value. 345 // 346 // if expiration is zero then giving value expiration will not be reset but left 347 // as is. 348 func (rd *RedisStore) UpdateTTL(key string, data []byte, expiration time.Duration) error { 349 var hashKey = rd.doHashKey(key) 350 var fstatus = rd.Client.SIsMember(rd.ctx, rd.hashList, hashKey) 351 if err := fstatus.Err(); err != nil { 352 return nerror.WrapOnly(err) 353 } 354 if !fstatus.Val() { 355 return nerror.New("key does not exist") 356 } 357 358 var _, pipeErr = rd.Client.TxPipelined(rd.ctx, func(cl redis.Pipeliner) error { 359 if len(data) == 0 { 360 var dstatus = cl.Del(rd.ctx, hashKey) 361 if err := dstatus.Err(); err != nil { 362 return err 363 } 364 365 var zs redis.Z 366 zs.Score = 0 367 zs.Member = hashKey 368 var zstatus = cl.ZRem(rd.ctx, rd.hashZList, zs) 369 if err := zstatus.Err(); err != nil { 370 return err 371 } 372 373 return nil 374 } 375 376 var zs redis.Z 377 zs.Score = 0 378 zs.Member = hashKey 379 380 var zstatus = cl.ZAdd(rd.ctx, rd.hashZList, &zs) 381 if err := zstatus.Err(); err != nil { 382 return nerror.WrapOnly(err) 383 } 384 385 var nset = cl.Set(rd.ctx, hashKey, data, expiration) 386 if err := nset.Err(); err != nil { 387 return nerror.WrapOnly(err) 388 } 389 return nil 390 }) 391 392 if err := pipeErr; err != nil { 393 return nerror.WrapOnly(pipeErr) 394 } 395 return nil 396 } 397 398 // TTL returns current expiration time for giving key. 399 func (rd *RedisStore) TTL(key string) (time.Duration, error) { 400 var hashKey = rd.doHashKey(key) 401 var nstatus = rd.Client.PTTL(rd.ctx, hashKey) 402 if err := nstatus.Err(); err != nil { 403 return 0, nerror.WrapOnly(err) 404 } 405 if nstatus.Val() < 0 { 406 return 0, nil 407 } 408 return nstatus.Val(), nil 409 } 410 411 // ExtendTTL extends the expiration of a giving key if it exists, the duration is expected to be 412 // in milliseconds. If expiration value is zero then we consider that you wish to remove the expiration. 413 func (rd *RedisStore) ExtendTTL(key string, expiration time.Duration) error { 414 var hashKey = rd.doHashKey(key) 415 var nstatus = rd.Client.PTTL(rd.ctx, hashKey) 416 if err := nstatus.Err(); err != nil { 417 return nerror.WrapOnly(err) 418 } 419 420 if nstatus.Val() < 0 { 421 return nil 422 } 423 424 var newExpiration = expiration + nstatus.Val() 425 var _, pipeErr = rd.Client.TxPipelined(rd.ctx, func(cl redis.Pipeliner) error { 426 if expiration == 0 { 427 var exstatus = cl.Persist(rd.ctx, hashKey) 428 return exstatus.Err() 429 } 430 431 var exstatus = cl.Expire(rd.ctx, hashKey, newExpiration) 432 return exstatus.Err() 433 }) 434 435 if err := pipeErr; err != nil { 436 return nerror.WrapOnly(pipeErr) 437 } 438 439 return nil 440 } 441 442 // ResetTTL resets giving expiration value to provided duration. 443 // 444 // A duration of zero persists the giving key. 445 func (rd *RedisStore) ResetTTL(key string, expiration time.Duration) error { 446 var hashKey = rd.doHashKey(key) 447 var nstatus = rd.Client.PTTL(rd.ctx, hashKey) 448 if err := nstatus.Err(); err != nil { 449 return nerror.WrapOnly(err) 450 } 451 452 if nstatus.Val() < 0 { 453 return nil 454 } 455 456 var _, pipeErr = rd.Client.TxPipelined(rd.ctx, func(cl redis.Pipeliner) error { 457 if expiration == 0 { 458 var exstatus = cl.Persist(rd.ctx, hashKey) 459 return exstatus.Err() 460 } 461 462 var exstatus = cl.Expire(rd.ctx, hashKey, expiration) 463 return exstatus.Err() 464 }) 465 if err := pipeErr; err != nil { 466 return nerror.WrapOnly(pipeErr) 467 } 468 469 return nil 470 } 471 472 // GetAnyKeys returns a list of values for any of the key's found. 473 // Unless a specific error occurred retrieving the value of a key, if a 474 // key is not found then it is ignored and a nil is set in it's place. 475 func (rd *RedisStore) GetAnyKeys(keys ...string) ([][]byte, error) { 476 var modifiedKeys = make([]string, len(keys)) 477 for index, key := range keys { 478 modifiedKeys[index] = rd.doHashKey(key) 479 } 480 481 var nstatus = rd.Client.MGet(rd.ctx, modifiedKeys...) 482 if err := nstatus.Err(); err != nil { 483 return nil, nerror.WrapOnly(err) 484 } 485 486 var values = make([][]byte, len(keys)) 487 var contentList = nstatus.Val() 488 for index, val := range contentList { 489 switch mv := val.(type) { 490 case string: 491 values[index] = nunsafe.String2Bytes(mv) 492 case []byte: 493 values[index] = mv 494 default: 495 values[index] = nil 496 } 497 } 498 return values, nil 499 } 500 501 // GetAllKeys returns a list of values for any of the key's found. 502 // if the value of a key is not found then we stop immediately, returning 503 // an error and the current set of items retreived. 504 func (rd *RedisStore) GetAllKeys(keys ...string) ([][]byte, error) { 505 var modifiedKeys = make([]string, len(keys)) 506 for index, key := range keys { 507 modifiedKeys[index] = rd.doHashKey(key) 508 } 509 510 var nstatus = rd.Client.MGet(rd.ctx, modifiedKeys...) 511 if err := nstatus.Err(); err != nil { 512 return nil, nerror.WrapOnly(err) 513 } 514 515 var values = make([][]byte, len(keys)) 516 var contentList = nstatus.Val() 517 for index, val := range contentList { 518 switch mv := val.(type) { 519 case string: 520 values[index] = nunsafe.String2Bytes(mv) 521 case []byte: 522 values[index] = mv 523 default: 524 return values, nerror.New("value with type %T has value %#v but is not bytes or string for key %q", mv, mv, keys[index]) 525 } 526 } 527 return values, nil 528 } 529 530 // Get returns giving session stored with giving key, returning an 531 // error if not found. 532 func (rd *RedisStore) Get(key string) ([]byte, error) { 533 var hashKey = rd.doHashKey(key) 534 var nstatus = rd.Client.Get(rd.ctx, hashKey) 535 if err := nstatus.Err(); err != nil { 536 return nil, nerror.WrapOnly(err) 537 } 538 return nunsafe.String2Bytes(nstatus.Val()), nil 539 } 540 541 // RemoveKeys removes underline key from the redis store after retrieving it and 542 // returning giving session. 543 func (rd *RedisStore) RemoveKeys(keys ...string) error { 544 var modifiedKeys = make([]string, len(keys)) 545 var modifiedIKeys = make([]interface{}, len(keys)) 546 547 for index, key := range keys { 548 var mod = rd.doHashKey(key) 549 modifiedKeys[index] = mod 550 modifiedIKeys[index] = mod 551 } 552 553 var _, err = rd.Client.TxPipelined(rd.ctx, func(pipeliner redis.Pipeliner) error { 554 var zstatus = pipeliner.ZRem(rd.ctx, rd.hashZList, modifiedIKeys...) 555 if err := zstatus.Err(); err != nil { 556 return err 557 } 558 var mstatus = pipeliner.SRem(rd.ctx, rd.hashList, modifiedIKeys...) 559 if err := mstatus.Err(); err != nil { 560 return nerror.WrapOnly(err) 561 } 562 var dstatus = pipeliner.Del(rd.ctx, modifiedKeys...) 563 if err := dstatus.Err(); err != nil { 564 return nerror.WrapOnly(err) 565 } 566 return nil 567 }) 568 if err != nil { 569 return nerror.WrapOnly(err) 570 } 571 return nil 572 } 573 574 // Remove removes underline key from the redis store after retrieving it and 575 // returning giving session. 576 func (rd *RedisStore) Remove(key string) ([]byte, error) { 577 var hashKey = rd.doHashKey(key) 578 var nstatus = rd.Client.Get(rd.ctx, hashKey) 579 if err := nstatus.Err(); err != nil { 580 return nil, nerror.WrapOnly(err) 581 } 582 583 var _, err = rd.Client.TxPipelined(rd.ctx, func(pipeliner redis.Pipeliner) error { 584 var zstatus = pipeliner.ZRem(rd.ctx, rd.hashZList, hashKey) 585 if err := zstatus.Err(); err != nil { 586 return nerror.WrapOnly(err) 587 } 588 var mstatus = pipeliner.SRem(rd.ctx, rd.hashList, hashKey) 589 if err := mstatus.Err(); err != nil { 590 return nerror.WrapOnly(err) 591 } 592 var dstatus = pipeliner.Del(rd.ctx, hashKey) 593 if err := dstatus.Err(); err != nil { 594 return nerror.WrapOnly(err) 595 } 596 return nil 597 }) 598 if err != nil { 599 return nil, nerror.WrapOnly(err) 600 } 601 return nunsafe.String2Bytes(nstatus.Val()), nil 602 }