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  }