Initial cloud-services repo - gateway service + pkg modules
This commit is contained in:
41
pkg/redis/batch_commands.go
Normal file
41
pkg/redis/batch_commands.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type RedisBatchCommands struct {
|
||||
Commands [][]interface{}
|
||||
}
|
||||
|
||||
func NewRedisBatchCommands() *RedisBatchCommands {
|
||||
result := RedisBatchCommands{}
|
||||
result.Commands = make([][]interface{}, 0)
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
func (rbc *RedisBatchCommands) Add(command ...interface{}) {
|
||||
rbc.Commands = append(rbc.Commands, command)
|
||||
}
|
||||
|
||||
func (rbc *RedisBatchCommands) AddPublish(key string, message interface{}) error {
|
||||
data, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
rbc.Add("PUBLISH", ChannelKey(key), data)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rbc *RedisBatchCommands) IsEmpty() bool {
|
||||
return len(rbc.Commands) == 0
|
||||
}
|
||||
|
||||
func (rbc *RedisBatchCommands) Clear() {
|
||||
rbc.Commands = make([][]interface{}, 0)
|
||||
}
|
||||
44
pkg/redis/client_pool.go
Normal file
44
pkg/redis/client_pool.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
func NewClientPool(args ...Pool) ClientPoolInterface {
|
||||
result := &ClientPool{}
|
||||
|
||||
if len(args) > 0 {
|
||||
result.pool = args[0]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
type ClientPoolInterface interface {
|
||||
GetPool() Pool
|
||||
SetPool(pool Pool)
|
||||
GetFromPool() Client
|
||||
}
|
||||
|
||||
type ClientPool struct {
|
||||
oncePool sync.Once
|
||||
pool Pool
|
||||
}
|
||||
|
||||
func (f *ClientPool) GetPool() Pool {
|
||||
f.oncePool.Do(func() {
|
||||
if f.pool == nil {
|
||||
f.pool = NewPool()
|
||||
}
|
||||
})
|
||||
|
||||
return f.pool
|
||||
}
|
||||
|
||||
func (f *ClientPool) SetPool(pool Pool) {
|
||||
f.pool = pool
|
||||
}
|
||||
|
||||
func (f *ClientPool) GetFromPool() Client {
|
||||
return NewClient(f.GetPool().Get())
|
||||
}
|
||||
927
pkg/redis/conn.go
Normal file
927
pkg/redis/conn.go
Normal file
@@ -0,0 +1,927 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"fiskerinc.com/modules/logger"
|
||||
"github.com/gomodule/redigo/redis"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// NewClient is constructor for connection method
|
||||
func NewClient(args ...redis.Conn) (client Client) {
|
||||
if len(args) > 0 {
|
||||
client = &Connection{
|
||||
conn: args[0],
|
||||
}
|
||||
} else {
|
||||
client = &Connection{}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Client defines the function signatures associated with sending messages
|
||||
//
|
||||
// and setting/getting objects
|
||||
type Client interface {
|
||||
GetConn() redis.Conn
|
||||
SetConn(redis.Conn)
|
||||
Close() error
|
||||
Ping() error
|
||||
|
||||
queueMessage(string, interface{}) error
|
||||
publishMessage(string, interface{}) error
|
||||
|
||||
BatchQueueMessages(ids []string, messages []interface{}) error
|
||||
BatchPublishMessages(ids []string, messages []interface{}) error
|
||||
|
||||
SafeQueueMessage(string, interface{}) error
|
||||
SafePublishMessage(string, interface{}) error
|
||||
|
||||
// Simple redis operations
|
||||
Set(string, interface{}) error
|
||||
Get(string) (interface{}, error)
|
||||
Delete(...interface{}) error
|
||||
GetMulti(ids []string) ([]interface{}, error)
|
||||
SetMulti(ids []string, data []interface{}) error
|
||||
|
||||
// Sets
|
||||
NewSet(string, interface{}, int) error
|
||||
GetSet(string, interface{}) error
|
||||
AddToSet(id string, data interface{}, expire int) error
|
||||
|
||||
// Use objects when you wish to access individual fields in future
|
||||
SetObject(string, interface{}, int) error
|
||||
SetObjectField(string, string, interface{}) error
|
||||
SetObjects([]string, []interface{}, int) error
|
||||
GetObject(string, interface{}) error
|
||||
GetObjectField(string, string) (string, error)
|
||||
GetObjectMap(string) (map[string]string, error)
|
||||
GetObjectRaw(string) (map[string][]byte, error)
|
||||
GetObjectsMulti([]string, []interface{}) error
|
||||
GetObjectsMultiMap([]string) (map[string]map[string]string, error)
|
||||
GetValuesMulti(ids []string, data interface{}) error
|
||||
|
||||
// General execution
|
||||
Retrieve(command string, data interface{}) error
|
||||
|
||||
// Cache functions marshal/unmarshal any data type to redis
|
||||
SetCache(string, interface{}, int) error
|
||||
GetCache(string, interface{}, int) error
|
||||
|
||||
// Thread-safe variations
|
||||
SafeSet(string, interface{}) error
|
||||
SafeGet(string) (interface{}, error)
|
||||
SafeDelete(...interface{}) error
|
||||
|
||||
SafeNewSet(string, interface{}, int) error
|
||||
SafeGetSet(string, interface{}) error
|
||||
|
||||
SafeSetObject(string, interface{}, int) error
|
||||
SafeGetObject(string, interface{}) error
|
||||
|
||||
Execute(command ...interface{}) (interface{}, error)
|
||||
SafeExecute(command ...interface{}) (interface{}, error)
|
||||
|
||||
ExecuteBatch(batch *RedisBatchCommands) (interface{}, error)
|
||||
SafeExecuteBatch(batch *RedisBatchCommands) (interface{}, error)
|
||||
}
|
||||
|
||||
// Connection holds a client to redis
|
||||
//
|
||||
// The methods for connection are NOT thread safe.
|
||||
type Connection struct {
|
||||
conn redis.Conn
|
||||
once sync.Once
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// GetConn creates a client if it doesn't exist
|
||||
func (c *Connection) GetConn() redis.Conn {
|
||||
c.once.Do(func() {
|
||||
if c.conn == nil {
|
||||
c.SetConn(NewPool().Get())
|
||||
}
|
||||
})
|
||||
return c.conn
|
||||
}
|
||||
|
||||
// SetConn sets the client
|
||||
func (c *Connection) SetConn(conn redis.Conn) {
|
||||
c.conn = conn
|
||||
}
|
||||
|
||||
// Close the client if it exists
|
||||
func (c *Connection) Close() error {
|
||||
if c.conn == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := c.conn.Close()
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
c.conn = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Connection) do(commandName string, args ...interface{}) (reply interface{}, err error) {
|
||||
reply, err = c.GetConn().Do(commandName, args...)
|
||||
if c.checkConnError(err) {
|
||||
return reply, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return reply, nil
|
||||
}
|
||||
|
||||
func (c *Connection) send(commandName string, args ...interface{}) error {
|
||||
err := c.GetConn().Send(commandName, args...)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Connection) Ping() error {
|
||||
_, err := c.GetConn().Do("PING")
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Connection) checkConnError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if c.isNetErr(err) {
|
||||
c.reconnect()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Connection) isNetErr(err error) bool {
|
||||
_, ok := errors.Cause(err).(*net.OpError)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (c *Connection) reconnect() {
|
||||
c.Close()
|
||||
c.SetConn(NewPool().Get())
|
||||
}
|
||||
|
||||
// QueueMessage writes a message to the corresponding list
|
||||
// follows the format "queue:<id>"
|
||||
// Messages are guaranteed to be delivered upon websocket connection
|
||||
func (c *Connection) queueMessage(id string, message interface{}) error {
|
||||
data, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
_, err = c.do("RPUSH", QueueKey(id), data)
|
||||
if err != nil {
|
||||
logger.At(logger.Error(), ChannelKey(id), "redis").
|
||||
Str("msg", string(data)).Err(err).Send()
|
||||
}
|
||||
|
||||
c.do("EXPIRE", QueueKey(id), 3600) // 3600 = 1hr
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// PublishMessage writes a message to the corresponding channel
|
||||
// follows the format "channel:<id>"
|
||||
// This is a fire and forget mechanism
|
||||
func (c *Connection) publishMessage(id string, message interface{}) error {
|
||||
data, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
_, err = c.do("PUBLISH", ChannelKey(id), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.At(logger.Debug(), ChannelKey(id), "redis").
|
||||
Str("msg", string(data)).
|
||||
Msgf("sent redis msg to %s", id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchQueueMessages is the same as QueueMessage except performs a batch call
|
||||
func (c *Connection) BatchQueueMessages(ids []string, messages []interface{}) error {
|
||||
if len(ids) != len(messages) {
|
||||
return errors.Errorf(
|
||||
"mismatch number of ids and messages. have %d ids and %d messages",
|
||||
len(ids),
|
||||
len(messages),
|
||||
)
|
||||
}
|
||||
|
||||
batch := NewRedisBatchCommands()
|
||||
for i := 0; i < len(ids); i++ {
|
||||
data, err := json.Marshal(messages[i])
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
batch.Add("RPUSH", QueueKey(ids[i]), data)
|
||||
}
|
||||
|
||||
_, err := c.ExecuteBatch(batch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchPublishMessages is the same as PublishMessage except performs a batch call
|
||||
func (c *Connection) BatchPublishMessages(ids []string, messages []interface{}) error {
|
||||
if len(ids) != len(messages) {
|
||||
return errors.Errorf(
|
||||
"mismatch number of ids and messages. have %d ids and %d messages",
|
||||
len(ids),
|
||||
len(messages),
|
||||
)
|
||||
}
|
||||
|
||||
batch := NewRedisBatchCommands()
|
||||
for i := 0; i < len(ids); i++ {
|
||||
data, err := json.Marshal(messages[i])
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
batch.Add("PUBLISH", ChannelKey(ids[i]), data)
|
||||
}
|
||||
|
||||
_, err := c.ExecuteBatch(batch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SafeQueueMessage is the thread-safe implementation of QueueMessage
|
||||
func (c *Connection) SafeQueueMessage(id string, message interface{}) error {
|
||||
// c.mu.Lock()
|
||||
// defer c.mu.Unlock()
|
||||
|
||||
return c.queueMessage(id, message)
|
||||
}
|
||||
|
||||
// SafePublishMessage is the thread-safe implementation of PublishMessage
|
||||
func (c *Connection) SafePublishMessage(id string, message interface{}) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
return c.publishMessage(id, message)
|
||||
}
|
||||
|
||||
// Set replicates redis "SET"
|
||||
func (c *Connection) Set(id string, data interface{}) error {
|
||||
if _, err := c.do("SET", id, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get replicates redis "GET", must properly unpack interface returned
|
||||
func (c *Connection) Get(id string) (interface{}, error) {
|
||||
data, err := c.do("GET", id)
|
||||
return data, err
|
||||
}
|
||||
|
||||
// Delete removes all ids inputted
|
||||
func (c *Connection) Delete(id ...interface{}) error {
|
||||
numDeleted, err := redis.Int(c.do("DEL", id...))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if numDeleted != len(id) {
|
||||
return errors.Errorf(
|
||||
"tried to delete %v (total: %v), however only %v were deleted",
|
||||
id, len(id), numDeleted,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deprecated: NewSet adds items to a set in redis
|
||||
func (c *Connection) NewSet(id string, data interface{}, expire int) error {
|
||||
var err error
|
||||
|
||||
if err = c.send("MULTI"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = c.send("DEL", id); err != nil {
|
||||
c.GetConn().Do("DISCARD")
|
||||
return err
|
||||
}
|
||||
|
||||
if err = c.send("SADD", redisArgs(id, data)...); err != nil {
|
||||
c.GetConn().Do("DISCARD")
|
||||
return err
|
||||
}
|
||||
|
||||
if expire > 0 {
|
||||
if err = c.send("EXPIRE", id, expire); err != nil {
|
||||
c.GetConn().Do("DISCARD")
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = c.do("EXEC"); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddToSet adds item to a set in redis
|
||||
func (c *Connection) AddToSet(id string, data interface{}, expire int) error {
|
||||
var err error
|
||||
|
||||
if err = c.send("MULTI"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = c.send("SADD", id, data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if expire > 0 {
|
||||
if err = c.send("EXPIRE", id, expire); err != nil {
|
||||
c.GetConn().Do("DISCARD")
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = c.do("EXEC"); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deprecated: GetSet retrieves items from a set in redis
|
||||
func (c *Connection) GetSet(id string, data interface{}) error {
|
||||
values, err := redis.Values(c.do("SMEMBERS", id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = redis.ScanSlice(values, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deprecated: SetObject assigns the hash key id to the data object
|
||||
// data can be of any type. Expire is in seconds, use -1 for no expire
|
||||
func (c *Connection) SetObject(id string, data interface{}, expire int) error {
|
||||
var err error
|
||||
|
||||
if expire > 0 {
|
||||
err = c.setExpiringObject(id, data, expire)
|
||||
} else {
|
||||
err = c.setPersistentObject(id, data)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Connection) setPersistentObject(id string, data interface{}) error {
|
||||
_, err := c.do("HSET", redisArgs(id, data)...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Connection) setExpiringObject(id string, data interface{}, expire int) error {
|
||||
var err error
|
||||
|
||||
if err = c.send("MULTI"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = c.send("HSET", redisArgs(id, data)...); err != nil {
|
||||
c.GetConn().Do("DISCARD")
|
||||
return err
|
||||
}
|
||||
|
||||
if err = c.send("EXPIRE", redisArgs(id, expire)...); err != nil {
|
||||
c.GetConn().Do("DISCARD")
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = c.do("EXEC"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deprecated: SetObjectField sets a specific key of an object
|
||||
func (c *Connection) SetObjectField(id string, key string, data interface{}) error {
|
||||
_, err := c.do("HSET", redisArgsMulti(id, key, data)...)
|
||||
return err
|
||||
}
|
||||
|
||||
// Deprecated: SetObjects provides the same functionality as SetObject for multiple objects in one call to redis
|
||||
func (c *Connection) SetObjects(ids []string, data []interface{}, expire int) error {
|
||||
var err error
|
||||
|
||||
if len(ids) <= 0 || len(data) <= 0 || len(ids) != len(data) {
|
||||
return errors.Errorf("invalid lengths entered, lengths must match and be > 0. ids length: %v data length: %v",
|
||||
len(ids),
|
||||
len(data),
|
||||
)
|
||||
}
|
||||
|
||||
if err = c.send("MULTI"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range ids {
|
||||
if err = c.send("HSET", redisArgs(ids[i], data[i])...); err != nil {
|
||||
c.GetConn().Do("DISCARD")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if expire > 0 {
|
||||
for _, id := range ids {
|
||||
if err = c.send("EXPIRE", id, expire); err != nil {
|
||||
c.GetConn().Do("DISCARD")
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = c.do("EXEC"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deprecated: GetObject retrieves an object based off the hash key id
|
||||
// and "unmarshals" it to struct pointer given
|
||||
func (c *Connection) GetObject(id string, dest interface{}) error {
|
||||
values, err := redis.Values(c.do("HGETALL", id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = redis.ScanStruct(values, dest)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deprecated: GetObjectMap retrieves an object based off the hash key id
|
||||
// and returns it as a map[string]interface{}. GetObject()
|
||||
// is preferred if you know the object being retrieved
|
||||
func (c *Connection) GetObjectMap(id string) (map[string]string, error) {
|
||||
object, err := redis.StringMap(c.do("HGETALL", id))
|
||||
return object, err
|
||||
}
|
||||
|
||||
// Deprecated: GetObjectRaw retrieves an object based off the hash key id
|
||||
// and returns it as a map[string][]byte. Use this method when you have
|
||||
// a hash of objects that you want to unmarshal.
|
||||
func (c *Connection) GetObjectRaw(id string) (map[string][]byte, error) {
|
||||
var m map[string][]byte
|
||||
|
||||
values, err := redis.Values(c.do("HGETALL", id))
|
||||
if err != nil {
|
||||
return m, err
|
||||
}
|
||||
if len(values)%2 != 0 {
|
||||
return nil, errors.New("GetObjectRaw expects even number of values in result")
|
||||
}
|
||||
|
||||
m = make(map[string][]byte, len(values)/2)
|
||||
for i := 0; i < len(values); i += 2 {
|
||||
key, okKey := values[i].([]byte)
|
||||
value, okValue := values[i+1].([]byte)
|
||||
if !okKey || !okValue {
|
||||
return nil, errors.New("cannot parse object into map[string][]byte")
|
||||
}
|
||||
m[string(key)] = value
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Deprecated: GetObjectField retrieves the value associated with the id and key of a hash map
|
||||
func (c *Connection) GetObjectField(id string, key string) (string, error) {
|
||||
value, err := redis.String(c.do("HGET", redisArgs(id, key)...))
|
||||
return value, err
|
||||
}
|
||||
|
||||
// Deprecated: GetObjectsMulti retrieves an array of objects based off
|
||||
// the hash key ids given and returns the data as a
|
||||
// []interface{}. Use this function if you know
|
||||
// you need to get multiple objects from redis (one call to server).
|
||||
func (c *Connection) GetObjectsMulti(ids []string, data []interface{}) error {
|
||||
var err error
|
||||
|
||||
if len(ids) != len(data) {
|
||||
return errors.Errorf(
|
||||
"number of ids given %v does not match size of data slice %v",
|
||||
len(ids),
|
||||
len(data),
|
||||
)
|
||||
}
|
||||
|
||||
if err = c.send("MULTI"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
if err = c.send("HGETALL", id); err != nil {
|
||||
c.GetConn().Do("DISCARD")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
values, err := redis.Values(c.do("EXEC"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i, value := range values {
|
||||
s, err := redis.Values(value, nil)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
if err = redis.ScanStruct(s, data[i]); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deprecated: GetObjectsMultiMap retrieves an array of objects based off
|
||||
// the hash key ids given and returns the data as a
|
||||
// map[string]interface{}. Use this function if you know
|
||||
// you need to get multiple objects from redis (one call to server).
|
||||
func (c *Connection) GetObjectsMultiMap(ids []string) (map[string]map[string]string, error) {
|
||||
var err error
|
||||
|
||||
objects := make(map[string]map[string]string)
|
||||
|
||||
if err := c.send("MULTI"); err != nil {
|
||||
return objects, err
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
if err = c.send("HGETALL", id); err != nil {
|
||||
c.GetConn().Do("DISCARD")
|
||||
return objects, err
|
||||
}
|
||||
}
|
||||
|
||||
values, err := redis.Values(c.do("EXEC"))
|
||||
if err != nil {
|
||||
c.GetConn().Do("DISCARD")
|
||||
return objects, err
|
||||
}
|
||||
|
||||
for i, value := range values {
|
||||
o, err := redis.StringMap(value, nil)
|
||||
if err != nil {
|
||||
return objects, errors.WithStack(err)
|
||||
}
|
||||
objects[ids[i]] = o
|
||||
}
|
||||
|
||||
return objects, nil
|
||||
}
|
||||
|
||||
func (c *Connection) makeKeys(ids []string) []interface{} {
|
||||
keys := make([]interface{}, len(ids))
|
||||
|
||||
for i, id := range ids {
|
||||
keys[i] = id
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
// Deprecated: GetMulti
|
||||
func (c *Connection) GetMulti(ids []string) ([]interface{}, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, errors.WithStack(errors.New("cannot call redis MGET with no keys"))
|
||||
}
|
||||
|
||||
keys := c.makeKeys(ids)
|
||||
result, err := redis.Values(c.do("MGET", keys...))
|
||||
if err != nil {
|
||||
c.GetConn().Do("DISCARD")
|
||||
return result, errors.WithStack(err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetValuesMulti retrieves an array of strings based off
|
||||
// the hash key ids given and returns the data as a
|
||||
// []string. Use this function if you know
|
||||
// you need to get multiple values from redis (one call to server).
|
||||
func (c *Connection) GetValuesMulti(ids []string, data interface{}) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
values, err := c.GetMulti(ids)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
err = redis.ScanSlice(values, data)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deprecated: SetMulti
|
||||
func (c *Connection) SetMulti(ids []string, data []interface{}) error {
|
||||
if len(ids) != len(data) {
|
||||
return errors.New("id and data lengths do not match")
|
||||
}
|
||||
|
||||
cmds := make([]interface{}, 2*len(ids))
|
||||
for i, id := range ids {
|
||||
cmds[2*i] = id
|
||||
cmds[2*i+1] = data[i]
|
||||
}
|
||||
|
||||
_, err := c.do("MSET", redisArgsMulti(cmds...)...)
|
||||
return err
|
||||
}
|
||||
|
||||
// Deprecated: Retrieve allows for manual commands and providing a destination interface{} to
|
||||
// deserialize bytes retrieved from redis
|
||||
func (c *Connection) Retrieve(command string, dest interface{}) error {
|
||||
input := strings.Split(command, " ")
|
||||
|
||||
if len(input) < 2 {
|
||||
return ErrInvalidCommand
|
||||
}
|
||||
|
||||
keyword := input[0]
|
||||
cmds := make([]interface{}, len(input)-1)
|
||||
for i, in := range input[1:] {
|
||||
cmds[i] = in
|
||||
}
|
||||
|
||||
reply, err := redis.Bytes(c.do(keyword, redisArgsMulti(cmds...)...))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(reply, dest)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deprecated: SetCache marshals object and inserts into redis based on key id
|
||||
// sets expiration to expire int
|
||||
func (c *Connection) SetCache(id string, data interface{}, expire int) error {
|
||||
var err error
|
||||
|
||||
serialized, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
if err = c.send("MULTI"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = c.send("SET", id, serialized); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if expire > 0 {
|
||||
if err = c.send("EXPIRE", id, expire); err != nil {
|
||||
c.GetConn().Do("DISCARD")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err = c.do("EXEC"); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deprecated: GetCache retrieves object from redis and unmarshals into dest (must be pointer)
|
||||
// resets expiration to expire int
|
||||
func (c *Connection) GetCache(id string, dest interface{}, expire int) error {
|
||||
var err error
|
||||
|
||||
if err = c.send("MULTI"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = c.send("GET", id); err != nil {
|
||||
c.GetConn().Do("DISCARD")
|
||||
return err
|
||||
}
|
||||
|
||||
if expire > 0 {
|
||||
|
||||
if err = c.send("EXPIRE", id, expire); err != nil {
|
||||
c.GetConn().Do("DISCARD")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
values, err := redis.Values(c.do("EXEC"))
|
||||
if err != nil || len(values) == 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
if values[0] == nil {
|
||||
return ErrNilObject
|
||||
}
|
||||
|
||||
data, err := redis.Bytes(values[0], nil)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(data, dest)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Deprecated: SafeSet is the thread-safe version of Set()
|
||||
func (c *Connection) SafeSet(id string, data interface{}) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
return c.Set(id, data)
|
||||
}
|
||||
|
||||
// Deprecated: SafeGet is the thread-safe version of Get()
|
||||
func (c *Connection) SafeGet(id string) (interface{}, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
return c.Get(id)
|
||||
}
|
||||
|
||||
// Deprecated: SafeDelete is the thread-safe version of Delete()
|
||||
func (c *Connection) SafeDelete(id ...interface{}) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
return c.Delete(id...)
|
||||
}
|
||||
|
||||
// Deprecated: SafeNewSet is the thread-safe version of NewSet()
|
||||
func (c *Connection) SafeNewSet(id string, data interface{}, expire int) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
return c.NewSet(id, data, expire)
|
||||
}
|
||||
|
||||
// Deprecated: SafeGetSet is the thread-safe version of GetSet()
|
||||
func (c *Connection) SafeGetSet(id string, data interface{}) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
return c.GetSet(id, data)
|
||||
}
|
||||
|
||||
// Deprecated: SafeSetObject provides a thread-safe SetObject()
|
||||
func (c *Connection) SafeSetObject(id string, data interface{}, expire int) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
return c.SetObject(id, data, expire)
|
||||
}
|
||||
|
||||
// Deprecated: SafeGetObject provides a thread-safe GetObject()
|
||||
func (c *Connection) SafeGetObject(id string, data interface{}) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
return c.GetObject(id, data)
|
||||
}
|
||||
|
||||
// Execute provides an abstraction over redigo's Do method
|
||||
// use this method over specialized methods within this library
|
||||
func (c *Connection) Execute(command ...interface{}) (interface{}, error) {
|
||||
var reply interface{}
|
||||
if len(command) < 2 {
|
||||
return reply, ErrInvalidCommand
|
||||
}
|
||||
|
||||
reply, err := c.do(command[0].(string), command[1:]...)
|
||||
if err != nil {
|
||||
return reply, err
|
||||
}
|
||||
|
||||
return reply, nil
|
||||
}
|
||||
|
||||
// SafeExecute provides a thread-safe Execute method
|
||||
func (c *Connection) SafeExecute(command ...interface{}) (interface{}, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
return c.Execute(command)
|
||||
}
|
||||
|
||||
// ExecuteBatch sends all commands stored to Redis
|
||||
// removes all commands regardless of success or failure
|
||||
func (c *Connection) ExecuteBatch(batch *RedisBatchCommands) (interface{}, error) {
|
||||
if batch.IsEmpty() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return c.executeBatch(batch)
|
||||
}
|
||||
|
||||
func (c *Connection) SafeExecuteBatch(batch *RedisBatchCommands) (interface{}, error) {
|
||||
if batch.IsEmpty() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
return c.executeBatch(batch)
|
||||
}
|
||||
|
||||
func (c *Connection) executeBatch(batch *RedisBatchCommands) (interface{}, error) {
|
||||
var reply interface{}
|
||||
var err error
|
||||
defer batch.Clear()
|
||||
|
||||
if err := c.send("MULTI"); err != nil {
|
||||
return reply, err
|
||||
}
|
||||
|
||||
for _, command := range batch.Commands {
|
||||
keyword, ok := command[0].(string)
|
||||
if !ok {
|
||||
c.GetConn().Do("DISCARD")
|
||||
return reply, ErrInvalidCommand
|
||||
}
|
||||
|
||||
if err := c.send(keyword, command[1:]...); err != nil {
|
||||
c.GetConn().Do("DISCARD")
|
||||
return reply, err
|
||||
}
|
||||
}
|
||||
|
||||
reply, err = c.do("EXEC")
|
||||
return reply, err
|
||||
}
|
||||
|
||||
// redisArgs is a helper function to generate redis args
|
||||
func redisArgs(id string, data interface{}) redis.Args {
|
||||
return redis.Args{}.Add(id).AddFlat(data)
|
||||
}
|
||||
|
||||
// redisArgsMulti is a helper function to generate redis args for hash map fields and arrays
|
||||
func redisArgsMulti(data ...interface{}) redis.Args {
|
||||
return redis.Args{}.Add(data...)
|
||||
}
|
||||
557
pkg/redis/conn_test.go
Normal file
557
pkg/redis/conn_test.go
Normal file
@@ -0,0 +1,557 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
m "fiskerinc.com/modules/common"
|
||||
"fiskerinc.com/modules/testhelper"
|
||||
|
||||
"github.com/gomodule/redigo/redis"
|
||||
)
|
||||
|
||||
func newMockConnClient() Client {
|
||||
return NewClient(GetMockPool().Get())
|
||||
}
|
||||
|
||||
func TestConnClient(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
conn := newMockConnClient()
|
||||
defer conn.Close()
|
||||
|
||||
c := conn.GetConn()
|
||||
conn.SetConn(c)
|
||||
|
||||
if c == nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnClient", "conn client", c)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnClose(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
conn := newMockConnClient()
|
||||
|
||||
_ = conn.GetConn()
|
||||
err := conn.Close()
|
||||
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnClose", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnQueueMessage(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
var conn Client = newMockConnClient()
|
||||
defer conn.Close()
|
||||
|
||||
err := conn.SafeQueueMessage("TESTVIN123", "hello fisker!")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnQueueMessage", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnPublishMessage(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
var conn Client = newMockConnClient()
|
||||
defer conn.Close()
|
||||
|
||||
err := conn.SafePublishMessage("TESTVIN123", "hello fisker!")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnPublishMessage", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnBatchQueueMessages(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
var conn Client = newMockConnClient()
|
||||
defer conn.Close()
|
||||
|
||||
err := conn.BatchQueueMessages(
|
||||
[]string{"TESTVIN123", "TESTVIN456"},
|
||||
[]interface{}{"hello ocean!", "hello pear!"})
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnBatchQueueMessages", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnBatchPublishMessages(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
var conn Client = newMockConnClient()
|
||||
defer conn.Close()
|
||||
|
||||
err := conn.BatchPublishMessages(
|
||||
[]string{"TESTVIN123", "TESTVIN456"},
|
||||
[]interface{}{"hello ocean!", "hello pear!"})
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnBatchPublishMessages", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnSafeQueueMessage(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
var conn Client = newMockConnClient()
|
||||
defer conn.Close()
|
||||
|
||||
err := conn.SafeQueueMessage("TESTVIN123", "hello fisker!")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnQueueMessage", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnSafePublishMessage(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
var conn Client = newMockConnClient()
|
||||
defer conn.Close()
|
||||
|
||||
err := conn.SafePublishMessage("TESTVIN123", "hello fisker!")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnPublishMessage", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnSet(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
var conn Client = newMockConnClient()
|
||||
defer conn.Close()
|
||||
|
||||
err := conn.Set("TESTKEY", true)
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnSet", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnGet(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
var conn Client = newMockConnClient()
|
||||
defer conn.Close()
|
||||
|
||||
_, err := conn.Get("TESTKEY")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnGet", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnDelete(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
var conn Client = newMockConnClient()
|
||||
defer conn.Close()
|
||||
|
||||
err := conn.Delete("TESTKEY")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnDelete", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnNewSet(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
var conn Client = newMockConnClient()
|
||||
defer conn.Close()
|
||||
|
||||
err := conn.NewSet("TESTKEY", []string{"cognito-id-1", "cognito-id-2"}, 0)
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnNewSet", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnGetSet(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
var conn Client = newMockConnClient()
|
||||
defer conn.Close()
|
||||
|
||||
var ids []string
|
||||
err := conn.GetSet("TESTKEY", &ids)
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnGetSet", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnSetObjectStruct(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
var conn Client = newMockConnClient()
|
||||
defer conn.Close()
|
||||
|
||||
l := m.Locks{
|
||||
Driver: true,
|
||||
All: false,
|
||||
}
|
||||
|
||||
err := conn.SetObject("TESTVIN123:locks", &l, -1)
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnSetObjectStruct", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnSetObjectStructExpiring(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
var conn Client = newMockConnClient()
|
||||
defer conn.Close()
|
||||
|
||||
l := m.Locks{
|
||||
Driver: true,
|
||||
All: false,
|
||||
}
|
||||
|
||||
err := conn.SetObject("TESTVIN123:locks", &l, 100)
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnSetObjectStruct", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnSetObjectMap(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
var conn Client = newMockConnClient()
|
||||
defer conn.Close()
|
||||
|
||||
l := map[string]interface{}{
|
||||
"left_front": true,
|
||||
"left_rear": true,
|
||||
"right_front": false,
|
||||
"right_rear": false,
|
||||
"trunk": false,
|
||||
}
|
||||
|
||||
err := conn.SetObject("TESTVIN123:locks", l, -1)
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnSetObjectMap", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnSetObjectField(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
var conn Client = newMockConnClient()
|
||||
defer conn.Close()
|
||||
|
||||
location := m.Location{
|
||||
Altitude: 10,
|
||||
Longitude: 15,
|
||||
Latitude: 20,
|
||||
}
|
||||
|
||||
serialized, err := location.Marshal()
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnSetObjectField", nil, err)
|
||||
}
|
||||
|
||||
err = conn.SetObjectField(CarLocationsKey(), "TESTVIN123", serialized)
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnSetObjectField", nil, err)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnSetObjects(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
var conn Client = newMockConnClient()
|
||||
defer conn.Close()
|
||||
|
||||
ids := []string{"TESTKEY1", "TESTKEY2", "TESTKEY3"}
|
||||
objects := []interface{}{
|
||||
m.Locks{
|
||||
Driver: true,
|
||||
All: true,
|
||||
},
|
||||
m.Locks{
|
||||
Driver: false,
|
||||
All: false,
|
||||
},
|
||||
m.Locks{
|
||||
Driver: true,
|
||||
All: false,
|
||||
},
|
||||
}
|
||||
|
||||
err := conn.SetObjects(ids, objects, 60)
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnSetObjects", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnSetObjectsError(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
var conn Client = newMockConnClient()
|
||||
defer conn.Close()
|
||||
|
||||
ids := []string{"TESTKEY1", "TESTKEY2", "TESTKEY3"}
|
||||
objects := []interface{}{}
|
||||
|
||||
err := conn.SetObjects(ids, objects, 60)
|
||||
if err == nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnSetObjectsError", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnGetObjectStruct(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
var conn Client = newMockConnClient()
|
||||
defer conn.Close()
|
||||
|
||||
var o m.Locks
|
||||
err := conn.GetObject("TESTVIN123:locks", &o)
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnGetObjectStruct", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnGetObjectMap(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
client := newMockConnClient()
|
||||
defer client.Close()
|
||||
|
||||
_, err := client.GetObjectMap("TESTVIN123:locks")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnGetObjectMap", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnGetObjectRaw(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
client := newMockConnClient()
|
||||
defer client.Close()
|
||||
|
||||
_, err := client.GetObjectRaw(CarLocationsKey())
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnGetObjectRaw", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnGetObjectField(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
client := newMockConnClient()
|
||||
defer client.Close()
|
||||
|
||||
_, err := client.GetObjectField(CarLocationsKey(), "TESTVIN123")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnGetObjectField", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnGetObjectsMulti(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
client := newMockConnClient()
|
||||
defer client.Close()
|
||||
|
||||
keys := []string{"TESTVIN123:locks", CarUpdateStatusHashKey(1234)}
|
||||
os := []interface{}{&m.Locks{}, &m.CarUpdateProgress{}}
|
||||
|
||||
err := client.GetObjectsMulti(keys, os)
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnGetObjectsMulti", nil, err)
|
||||
}
|
||||
|
||||
if len(keys) != len(os) {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnGetObjectsMulti", len(keys), len(os))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnGetObjectsMultiMap(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
client := newMockConnClient()
|
||||
defer client.Close()
|
||||
|
||||
keys := []string{"TESTVIN123:locks", CarUpdateStatusHashKey(1234)}
|
||||
|
||||
os, err := client.GetObjectsMultiMap(keys)
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnGetObjectsMultiMap", nil, err)
|
||||
}
|
||||
|
||||
if os == nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnGetObjectsMultiMap", "map", os)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnGetValuesMulti(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
client := newMockConnClient()
|
||||
defer client.Close()
|
||||
|
||||
ids := []string{"test1", "test2", "test3"}
|
||||
data := make([]string, len(ids))
|
||||
err := client.GetValuesMulti(ids, &data)
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnGetValuesMulti", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnRetrieve(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
client := newMockConnClient()
|
||||
defer client.Close()
|
||||
|
||||
var value interface{}
|
||||
err := client.Retrieve("HGET test1", &value)
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnRetrieve", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnSafeSet(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
client := newMockConnClient()
|
||||
defer client.Close()
|
||||
|
||||
err := client.SafeSet("TESTKEY", true)
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnSafeSet", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnSafeGet(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
client := newMockConnClient()
|
||||
defer client.Close()
|
||||
|
||||
_, err := client.SafeGet("TESTKEY")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnSafeGet", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnSafeDelete(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
client := newMockConnClient()
|
||||
defer client.Close()
|
||||
|
||||
err := client.SafeDelete("TESTKEY")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnSafeDelete", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnSafeNewSet(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
client := newMockConnClient()
|
||||
defer client.Close()
|
||||
|
||||
err := client.SafeNewSet("TESTKEY", []string{"cognito-id-1", "cognito-id-2"}, 0)
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnSafeNewSet", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnSafeGetSet(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
client := newMockConnClient()
|
||||
defer client.Close()
|
||||
|
||||
var ids []string
|
||||
err := client.SafeGetSet("TESTKEY", &ids)
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnSafeGetSet", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnSafeSetObjectStruct(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
client := newMockConnClient()
|
||||
defer client.Close()
|
||||
|
||||
l := m.Locks{
|
||||
Driver: true,
|
||||
All: false,
|
||||
}
|
||||
|
||||
err := client.SafeSetObject("TESTVIN123:locks", &l, -1)
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnSafeSetObjectStruct", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnSafeGetObjectStruct(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
client := newMockConnClient()
|
||||
defer client.Close()
|
||||
|
||||
var o m.Locks
|
||||
err := client.SafeGetObject("TESTVIN123:locks", &o)
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnSafeGetObjectStruct", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnExecute(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
client := newMockConnClient()
|
||||
defer client.Close()
|
||||
|
||||
_, err := client.Execute("GET", "test_key")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnExecute", nil, err)
|
||||
}
|
||||
|
||||
_, err = client.Execute("SET", "test_key", "test_value")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnExecute", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnSafeExecute(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
client := newMockConnClient()
|
||||
defer client.Close()
|
||||
|
||||
_, err := client.Execute("HGETALL", "test_key")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnExecute", nil, err)
|
||||
}
|
||||
|
||||
_, err = client.Execute("HGET", "test_key", "test_field")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnExecute", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnAddToBatch(t *testing.T) {
|
||||
batch := NewRedisBatchCommands()
|
||||
batch.Add("SET", "test_key", "test_value")
|
||||
}
|
||||
|
||||
func TestConnExecuteBatch(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
client := newMockConnClient()
|
||||
defer client.Close()
|
||||
batch := NewRedisBatchCommands()
|
||||
|
||||
batch.Add("SET", "test_key", "test_value")
|
||||
result, err := redis.Values(client.ExecuteBatch(batch))
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnExecuteBatch", nil, err)
|
||||
}
|
||||
|
||||
if len(result) != 0 {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnExecuteBatch", 0, len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnSafeExecuteBatch(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
client := newMockConnClient()
|
||||
defer client.Close()
|
||||
batch := NewRedisBatchCommands()
|
||||
|
||||
batch.Add("SET", "test_key", "test_value")
|
||||
result, err := redis.Values(client.SafeExecuteBatch(batch))
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnExecuteBatch", nil, err)
|
||||
}
|
||||
|
||||
if len(result) != 0 {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnExecuteBatch", 0, len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConnBatchIsEmpty(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
client := newMockConnClient()
|
||||
defer client.Close()
|
||||
batch := NewRedisBatchCommands()
|
||||
|
||||
if !batch.IsEmpty() {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnBatchIsEmpty", true, false)
|
||||
}
|
||||
|
||||
batch.Add("SET", "test_key", "test_value")
|
||||
|
||||
if batch.IsEmpty() {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnBatchIsEmpty", false, true)
|
||||
}
|
||||
}
|
||||
12
pkg/redis/errors.go
Normal file
12
pkg/redis/errors.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var ErrHangingCommands = errors.New("commands left over in redis client")
|
||||
|
||||
var ErrInvalidCommand = errors.New("invalid command entered")
|
||||
var ErrInvalidResults = errors.New("invalid results returned in redis")
|
||||
|
||||
var ErrNilObject = errors.New("not found in redis")
|
||||
163
pkg/redis/keys.go
Normal file
163
pkg/redis/keys.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"runtime/debug"
|
||||
|
||||
"fiskerinc.com/modules/logger"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
channelPrefix = "channel"
|
||||
|
||||
SupersetAccTokenKey = "superset:access_token"
|
||||
)
|
||||
|
||||
// ChannelKey provides hash value for redis channel
|
||||
func ChannelKey(id string) string {
|
||||
return fmt.Sprintf("%s:%s", channelPrefix, id)
|
||||
}
|
||||
|
||||
// ParseChannelKey returns id from hash value
|
||||
func ParseChannelKey(key string) string {
|
||||
return key[len(channelPrefix)+1:]
|
||||
}
|
||||
|
||||
const queuePrefix = "queue"
|
||||
|
||||
// QueueKey provides hash value for redis queue
|
||||
func QueueKey(id string) string {
|
||||
if id == "" {
|
||||
stack := debug.Stack()
|
||||
logger.Warn().Str("Queue", queuePrefix).Str("ID", id).Str("Stack", string(stack)).Msg("Creating Redis Queue Key Empty")
|
||||
|
||||
}
|
||||
return fmt.Sprintf("%s:%s", queuePrefix, id)
|
||||
}
|
||||
|
||||
// ParseQueueKey returns id from hash value
|
||||
func ParseQueueKey(key string) string {
|
||||
return key[len(queuePrefix)+1:]
|
||||
}
|
||||
|
||||
func CarConfigKey(vin string) string {
|
||||
return fmt.Sprintf("car:%s:config", vin)
|
||||
}
|
||||
|
||||
func CarLogFilter(vin string) string {
|
||||
return fmt.Sprintf("car:%s:state:log", vin)
|
||||
}
|
||||
|
||||
// WindowsHashKey provides hash key lookup string for windows state
|
||||
func WindowsHashKey(vin string) string {
|
||||
return fmt.Sprintf("car:%s:state:windows", vin)
|
||||
}
|
||||
|
||||
// CarStateHashKey provides hash key structure for car state
|
||||
//
|
||||
// car state values such as windows, locks, etc. are stored as
|
||||
// keys in the redis hash map
|
||||
func CarStateHashKey(vin string) string {
|
||||
return fmt.Sprintf("car:%s:state", vin)
|
||||
}
|
||||
|
||||
// CarAlerts returns key of expiring cache of sent car alerts.
|
||||
func CarAlerts(vin string) string {
|
||||
return fmt.Sprintf("car:%s:alerts", vin)
|
||||
}
|
||||
|
||||
// CANParseSignalWarnings returns key of expiring cache of unknown signals.
|
||||
func CANParseSignalWarnings(version string) string {
|
||||
return fmt.Sprintf("can:version:%s:signals:warn", version)
|
||||
}
|
||||
|
||||
// CarToDriverKey provides hash key lookup for driver associated with car
|
||||
func CarToDriverKey(vin string, id string) string {
|
||||
return fmt.Sprintf("car:%s:driver:%s", vin, id)
|
||||
}
|
||||
|
||||
// CarToAllDriversKey provides hash key lookup for drivers associated with car
|
||||
func CarToAllDriversKey(vin string) string {
|
||||
return fmt.Sprintf("car:%s:drivers", vin)
|
||||
}
|
||||
|
||||
// CarSessionsKey provides a set of all cars in session
|
||||
func CarSessionsKey() string {
|
||||
return "cars:sessions"
|
||||
}
|
||||
|
||||
func CarLocationsKey() string {
|
||||
return "cars:locations"
|
||||
}
|
||||
|
||||
// CarUpdateStatusHashKey provides hash key lookup string for UpdateStatus
|
||||
func CarUpdateStatusHashKey(carupdateid int64) string {
|
||||
return fmt.Sprintf("carupdate:%v", carupdateid)
|
||||
}
|
||||
|
||||
// CarUpdateStatusHMIHashKey provides hash key lookup string for UpdateStatus
|
||||
func CarUpdateStatusTBOXHashKey(carupdateid int64) string {
|
||||
return fmt.Sprintf("carupdatetbox:%v", carupdateid)
|
||||
}
|
||||
|
||||
// CarUpdateStatusHMIHashKey provides hash key lookup string for UpdateStatus
|
||||
func CarUpdateStatusHMIHashKey(carupdateid int64) string {
|
||||
return fmt.Sprintf("carupdatehmi:%v", carupdateid)
|
||||
}
|
||||
|
||||
func DriverToVINsKey(id string) string {
|
||||
return fmt.Sprintf("driver:%s:cars", id)
|
||||
}
|
||||
|
||||
// HMISessionsKey provides a set of all HMIs in session
|
||||
func HMISessionsKey() string {
|
||||
return "hmi:sessions"
|
||||
}
|
||||
|
||||
// HMIManySessionsKey provides a set of all sessions cloud believes are open
|
||||
func HMIManySessionsKey(vin string) string {
|
||||
return fmt.Sprintf("hmi:%s:many-sessions", vin)
|
||||
}
|
||||
|
||||
// HMISessionKey provides hash key lookup for HMI session key
|
||||
func HMISessionKey(vin string) string {
|
||||
return fmt.Sprintf("hmi:%s:session", vin)
|
||||
}
|
||||
|
||||
func HMISaltKey(vin string) string {
|
||||
return fmt.Sprintf("hmi:%s:salt", vin)
|
||||
}
|
||||
|
||||
// FileIDEncryptionParamsKey provides hash key lookup string for file encryption parameters
|
||||
func FileIDEncryptionParamsKey(fileid string) string {
|
||||
return fmt.Sprintf("fileid:%s", fileid)
|
||||
}
|
||||
|
||||
// MobileSessionsKey provides a set of all mobiles in session
|
||||
func MobileSessionsKey() string {
|
||||
return "mobile:sessions"
|
||||
}
|
||||
|
||||
// APITokenKey provides hash key lookup string
|
||||
func APITokenKey(key string) string {
|
||||
return fmt.Sprintf("apikey:%s", key)
|
||||
}
|
||||
|
||||
func SubscriptionTypeListKey(subtypeID uuid.UUID) string {
|
||||
return fmt.Sprintf("subscriptiontypes:%s", subtypeID.String())
|
||||
}
|
||||
|
||||
// TimezoneQuadKey provides hash key lookup string
|
||||
func TimezoneQuadKey(quadkey string, zoom int) string {
|
||||
trim := min(len(quadkey), zoom)
|
||||
return fmt.Sprintf("timezone:%s", quadkey[:trim])
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
15
pkg/redis/listener.go
Normal file
15
pkg/redis/listener.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package redis
|
||||
|
||||
import "context"
|
||||
|
||||
// Listener provides the interface for all redis queues/pubsub
|
||||
type Listener interface {
|
||||
Add(string) error
|
||||
Remove(string) error
|
||||
|
||||
Listen(context.Context, func(string, []byte) error) error
|
||||
ListenChannel() error
|
||||
|
||||
Length() int
|
||||
Restart() error
|
||||
}
|
||||
89
pkg/redis/mock.go
Normal file
89
pkg/redis/mock.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
mock_redis "fiskerinc.com/modules/redis/mock"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/gomodule/redigo/redis"
|
||||
)
|
||||
|
||||
var pool Pool
|
||||
var mockHashMap map[string]interface{}
|
||||
|
||||
type timeoutTestConn int
|
||||
|
||||
// GetMockPool returns the singleton pool (for mocking)
|
||||
func GetMockPool() Pool {
|
||||
if pool != nil {
|
||||
return pool
|
||||
}
|
||||
|
||||
pool = &redis.Pool{
|
||||
Dial: func() (redis.Conn, error) { return timeoutTestConn(0), nil },
|
||||
}
|
||||
|
||||
return pool
|
||||
}
|
||||
|
||||
// SetMockPool sets the redis pool (ideal for mocking)
|
||||
func SetMockPool(p Pool) {
|
||||
pool = p
|
||||
}
|
||||
|
||||
func (tc timeoutTestConn) Do(c string, d ...interface{}) (interface{}, error) {
|
||||
switch c {
|
||||
case "GET":
|
||||
return "XXXXX", nil
|
||||
case "DEL":
|
||||
return []byte(strconv.Itoa(len(d))), nil
|
||||
case "HSET":
|
||||
mockHashMap[d[0].(string)] = d[1:]
|
||||
return time.Duration(-1), nil
|
||||
case "HGET":
|
||||
payload, _ := json.Marshal("XXXXX")
|
||||
return payload, nil
|
||||
case "HGETALL", "EXEC", "SMEMBERS", "MGET":
|
||||
return []interface{}{}, nil
|
||||
case "SMISMEMBER":
|
||||
return []interface{}{"0", "1"}, nil
|
||||
}
|
||||
|
||||
return time.Duration(-1), nil
|
||||
}
|
||||
func (tc timeoutTestConn) DoWithTimeout(timeout time.Duration, cmd string, args ...interface{}) (interface{}, error) {
|
||||
return timeout, nil
|
||||
}
|
||||
|
||||
func (tc timeoutTestConn) Receive() (interface{}, error) {
|
||||
return time.Duration(-1), nil
|
||||
}
|
||||
func (tc timeoutTestConn) ReceiveWithTimeout(timeout time.Duration) (interface{}, error) {
|
||||
return timeout, nil
|
||||
}
|
||||
|
||||
func (tc timeoutTestConn) Send(string, ...interface{}) error { return nil }
|
||||
func (tc timeoutTestConn) Err() error { return nil }
|
||||
func (tc timeoutTestConn) Close() error { return nil }
|
||||
func (tc timeoutTestConn) Flush() error { return nil }
|
||||
|
||||
// MockRedisConnection creates a mock pool with mock connections
|
||||
func MockRedisConnection() {
|
||||
mockPool()
|
||||
mockHashMap = make(map[string]interface{})
|
||||
}
|
||||
|
||||
func mockPool() {
|
||||
pool = &redis.Pool{
|
||||
Dial: func() (redis.Conn, error) { return timeoutTestConn(0), nil },
|
||||
}
|
||||
}
|
||||
|
||||
func InitMockPool(ctrl *gomock.Controller, mockClient redis.Conn) {
|
||||
mockPool := mock_redis.NewMockPool(ctrl)
|
||||
mockPool.EXPECT().Get().Return(mockClient)
|
||||
SetMockPool(mockPool)
|
||||
}
|
||||
218
pkg/redis/mock/conn.go
Normal file
218
pkg/redis/mock/conn.go
Normal file
@@ -0,0 +1,218 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: pool.go
|
||||
|
||||
// Package mock_redis is a generated GoMock package.
|
||||
package mock_redis
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
time "time"
|
||||
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
redis "github.com/gomodule/redigo/redis"
|
||||
)
|
||||
|
||||
// MockPool is a mock of Pool interface.
|
||||
type MockPool struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockPoolMockRecorder
|
||||
}
|
||||
|
||||
// MockPoolMockRecorder is the mock recorder for MockPool.
|
||||
type MockPoolMockRecorder struct {
|
||||
mock *MockPool
|
||||
}
|
||||
|
||||
// NewMockPool creates a new mock instance.
|
||||
func NewMockPool(ctrl *gomock.Controller) *MockPool {
|
||||
mock := &MockPool{ctrl: ctrl}
|
||||
mock.recorder = &MockPoolMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockPool) EXPECT() *MockPoolMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Get mocks base method.
|
||||
func (m *MockPool) Get() redis.Conn {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Get")
|
||||
ret0, _ := ret[0].(redis.Conn)
|
||||
return ret0
|
||||
}
|
||||
|
||||
func (m *MockPool) Stats() redis.PoolStats {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Stats")
|
||||
ret0, _ := ret[0].(redis.PoolStats)
|
||||
return ret0
|
||||
}
|
||||
|
||||
func (m *MockPool) ActiveCount() int {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ActiveCount")
|
||||
ret0, _ := ret[0].(int)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Get indicates an expected call of Get.
|
||||
func (mr *MockPoolMockRecorder) Get() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockPool)(nil).Get))
|
||||
}
|
||||
|
||||
// MockConn is a mock of Conn interface.
|
||||
type MockConn struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockConnMockRecorder
|
||||
}
|
||||
|
||||
// MockConnMockRecorder is the mock recorder for MockConn.
|
||||
type MockConnMockRecorder struct {
|
||||
mock *MockConn
|
||||
}
|
||||
|
||||
// NewMockConn creates a new mock instance.
|
||||
func NewMockConn(ctrl *gomock.Controller) *MockConn {
|
||||
mock := &MockConn{ctrl: ctrl}
|
||||
mock.recorder = &MockConnMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
||||
func (m *MockConn) EXPECT() *MockConnMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// Close mocks base method.
|
||||
func (m *MockConn) Close() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Close")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Close indicates an expected call of Close.
|
||||
func (mr *MockConnMockRecorder) Close() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockConn)(nil).Close))
|
||||
}
|
||||
|
||||
// Do mocks base method.
|
||||
func (m *MockConn) Do(arg0 string, arg1 ...interface{}) (interface{}, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []interface{}{arg0}
|
||||
for _, a := range arg1 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "Do", varargs...)
|
||||
ret0, _ := ret[0].(interface{})
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Do indicates an expected call of Do.
|
||||
func (mr *MockConnMockRecorder) Do(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]interface{}{arg0}, arg1...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockConn)(nil).Do), varargs...)
|
||||
}
|
||||
|
||||
// DoWithTimeout mocks base method.
|
||||
func (m *MockConn) DoWithTimeout(arg0 time.Duration, arg1 string, arg2 ...interface{}) (interface{}, error) {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []interface{}{arg0, arg1}
|
||||
for _, a := range arg2 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "DoWithTimeout", varargs...)
|
||||
ret0, _ := ret[0].(interface{})
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// DoWithTimeout indicates an expected call of DoWithTimeout.
|
||||
func (mr *MockConnMockRecorder) DoWithTimeout(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]interface{}{arg0, arg1}, arg2...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoWithTimeout", reflect.TypeOf((*MockConn)(nil).DoWithTimeout), varargs...)
|
||||
}
|
||||
|
||||
// Err mocks base method.
|
||||
func (m *MockConn) Err() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Err")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Err indicates an expected call of Err.
|
||||
func (mr *MockConnMockRecorder) Err() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Err", reflect.TypeOf((*MockConn)(nil).Err))
|
||||
}
|
||||
|
||||
// Flush mocks base method.
|
||||
func (m *MockConn) Flush() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Flush")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Flush indicates an expected call of Flush.
|
||||
func (mr *MockConnMockRecorder) Flush() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Flush", reflect.TypeOf((*MockConn)(nil).Flush))
|
||||
}
|
||||
|
||||
// Receive mocks base method.
|
||||
func (m *MockConn) Receive() (interface{}, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "Receive")
|
||||
ret0, _ := ret[0].(interface{})
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// Receive indicates an expected call of Receive.
|
||||
func (mr *MockConnMockRecorder) Receive() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Receive", reflect.TypeOf((*MockConn)(nil).Receive))
|
||||
}
|
||||
|
||||
// ReceiveWithTimeout mocks base method.
|
||||
func (m *MockConn) ReceiveWithTimeout(arg0 time.Duration) (interface{}, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ReceiveWithTimeout", arg0)
|
||||
ret0, _ := ret[0].(interface{})
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ReceiveWithTimeout indicates an expected call of ReceiveWithTimeout.
|
||||
func (mr *MockConnMockRecorder) ReceiveWithTimeout(arg0 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReceiveWithTimeout", reflect.TypeOf((*MockConn)(nil).ReceiveWithTimeout), arg0)
|
||||
}
|
||||
|
||||
// Send mocks base method.
|
||||
func (m *MockConn) Send(arg0 string, arg1 ...interface{}) error {
|
||||
m.ctrl.T.Helper()
|
||||
varargs := []interface{}{arg0}
|
||||
for _, a := range arg1 {
|
||||
varargs = append(varargs, a)
|
||||
}
|
||||
ret := m.ctrl.Call(m, "Send", varargs...)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// Send indicates an expected call of Send.
|
||||
func (mr *MockConnMockRecorder) Send(arg0 interface{}, arg1 ...interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
varargs := append([]interface{}{arg0}, arg1...)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Send", reflect.TypeOf((*MockConn)(nil).Send), varargs...)
|
||||
}
|
||||
79
pkg/redis/pool.go
Normal file
79
pkg/redis/pool.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"fiskerinc.com/modules/logger"
|
||||
"fiskerinc.com/modules/utils/envtool"
|
||||
|
||||
"github.com/gomodule/redigo/redis"
|
||||
"github.com/pkg/errors"
|
||||
redigotrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/gomodule/redigo"
|
||||
)
|
||||
|
||||
// connection vars
|
||||
var (
|
||||
host = envtool.GetEnv("REDIS_HOST", "localhost")
|
||||
port = envtool.GetEnv("REDIS_PORT", "6379")
|
||||
password = envtool.GetEnv("REDIS_PASSWORD", "REPLACE_ME")
|
||||
addr = fmt.Sprintf("%v:%v", host, port)
|
||||
)
|
||||
|
||||
func UpdateRedisConnection(redisHost string, redisPort string, redisPassword string) {
|
||||
host = redisHost
|
||||
port = redisPort
|
||||
password = redisPassword
|
||||
addr = fmt.Sprintf("%v:%v", host, port)
|
||||
|
||||
}
|
||||
|
||||
// Pool provides redis connections
|
||||
type Pool interface {
|
||||
Get() redis.Conn
|
||||
Stats() redis.PoolStats
|
||||
ActiveCount() int
|
||||
}
|
||||
|
||||
func NewPool() Pool {
|
||||
return &redis.Pool{
|
||||
IdleTimeout: time.Millisecond * time.Duration(envtool.GetEnvInt("REDIS_IDLETIMEOUT_MS", 1000)),
|
||||
MaxIdle: envtool.GetEnvInt("REDIS_MAXIDLECONN", 10),
|
||||
MaxActive: envtool.GetEnvInt("REDIS_MAXACTIVECONN", 10),
|
||||
MaxConnLifetime: time.Millisecond * time.Duration(envtool.GetEnvInt("REDIS_MAXCONNLIFETIME_MS", 1000)),
|
||||
Wait: (envtool.GetEnvInt("REDIS_WAITGETCONN", 0) == 1),
|
||||
Dial: func() (redis.Conn, error) {
|
||||
conn, err := redigotrace.Dial("tcp", addr, redis.DialKeepAlive(time.Minute*time.Duration(envtool.GetEnvInt("REDIS_KEEPALIVE_MINS", 10))))
|
||||
if password == "" {
|
||||
return conn, errors.WithStack(err)
|
||||
}
|
||||
if err != nil {
|
||||
logger.Error().Err(errors.WithStack(err)).Send()
|
||||
return nil, err
|
||||
}
|
||||
if _, err := conn.Do("AUTH", password); err != nil {
|
||||
conn.Close()
|
||||
logger.Error().Err(errors.WithStack(err)).Send()
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
},
|
||||
TestOnBorrow: func(c redis.Conn, t time.Time) error {
|
||||
_, err := c.Do("PING")
|
||||
return err
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type Conn interface {
|
||||
Do(string, ...interface{}) (interface{}, error)
|
||||
DoWithTimeout(time.Duration, string, ...interface{}) (interface{}, error)
|
||||
|
||||
Receive() (interface{}, error)
|
||||
ReceiveWithTimeout(time.Duration) (interface{}, error)
|
||||
|
||||
Send(string, ...interface{}) error
|
||||
Err() error
|
||||
Close() error
|
||||
Flush() error
|
||||
}
|
||||
16
pkg/redis/pool_test.go
Normal file
16
pkg/redis/pool_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/testhelper"
|
||||
)
|
||||
|
||||
func TestRedisGetPool(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
p := GetMockPool()
|
||||
|
||||
if p != pool {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestRedisGetPool", pool, p)
|
||||
}
|
||||
}
|
||||
158
pkg/redis/pubsub.go
Normal file
158
pkg/redis/pubsub.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"fiskerinc.com/modules/logger"
|
||||
"github.com/gomodule/redigo/redis"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// NewPubSub generates a new working PubSub object
|
||||
func NewPubSub(args ...redis.Conn) *PubSub {
|
||||
var conn redis.Conn
|
||||
if len(args) > 0 {
|
||||
conn = args[0]
|
||||
} else {
|
||||
conn = NewClient().GetConn()
|
||||
}
|
||||
|
||||
return &PubSub{
|
||||
connection: redis.PubSubConn{Conn: conn},
|
||||
subscriptions: make(Set),
|
||||
}
|
||||
}
|
||||
|
||||
// PubSub is a struct used for subscribing to Redis channels
|
||||
//
|
||||
// follows the Listener interface
|
||||
type PubSub struct {
|
||||
connection PubSubClient
|
||||
subscriptions Set
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// PubSubClient provides necessary functions needed for connection
|
||||
//
|
||||
// within PubSub struct
|
||||
type PubSubClient interface {
|
||||
Receive() interface{}
|
||||
Subscribe(...interface{}) error
|
||||
Unsubscribe(...interface{}) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
// Add an ID to subscriber
|
||||
func (ps *PubSub) Add(id string) error {
|
||||
ps.mu.Lock()
|
||||
defer ps.mu.Unlock()
|
||||
|
||||
ok := ps.subscriptions.Add(id)
|
||||
if !ok {
|
||||
return errors.Errorf("%v already in subscriptions", id)
|
||||
}
|
||||
|
||||
if err := ps.connection.Subscribe(ChannelKey(id)); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove an ID from subscriber
|
||||
func (ps *PubSub) Remove(id string) error {
|
||||
ps.mu.Lock()
|
||||
defer ps.mu.Unlock()
|
||||
|
||||
ok := ps.subscriptions.Remove(id)
|
||||
if !ok {
|
||||
return errors.Errorf("%v does not exist in subscriptions", id)
|
||||
}
|
||||
|
||||
if err := ps.connection.Unsubscribe(ChannelKey(id)); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Listen loops on receiving messages from subscriptions until cancelled
|
||||
func (ps *PubSub) Listen(ctx context.Context, handler func(string, []byte) error) error {
|
||||
isListening := true
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
isListening = false
|
||||
break
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
|
||||
ps.mu.Lock()
|
||||
defer ps.mu.Unlock()
|
||||
if err := ps.connection.Unsubscribe(); err != nil {
|
||||
logger.Error().Err(err).Send()
|
||||
}
|
||||
}()
|
||||
|
||||
for isListening {
|
||||
switch m := ps.connection.Receive().(type) {
|
||||
case error:
|
||||
done <- m
|
||||
return errors.WithStack(m)
|
||||
case redis.Message:
|
||||
id := ParseChannelKey(m.Channel)
|
||||
go func(channel string, data []byte) {
|
||||
if err := handler(channel, data); err != nil {
|
||||
logger.At(logger.Error(), channel, "redis").
|
||||
Err(err).Send()
|
||||
} else {
|
||||
logger.At(logger.Debug(), channel, "redis").
|
||||
Str("msg", string(data)).
|
||||
Msgf("sent published msg to %s", channel)
|
||||
}
|
||||
}(id, m.Data)
|
||||
case redis.Subscription:
|
||||
switch m.Count {
|
||||
case 0:
|
||||
if !isListening {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListenChannel dumps redis messages to channel rather
|
||||
//
|
||||
// than using a traditional handler
|
||||
func (ps *PubSub) ListenChannel() error {
|
||||
// stub
|
||||
return nil
|
||||
}
|
||||
|
||||
// Length returns number of subscriptions
|
||||
func (ps *PubSub) Length() int {
|
||||
return len(ps.subscriptions)
|
||||
}
|
||||
|
||||
// Restart re-initializes pubsub connection
|
||||
func (ps *PubSub) Restart() error {
|
||||
ps.mu.Lock()
|
||||
defer ps.mu.Unlock()
|
||||
if ps.connection != nil {
|
||||
ps.connection.Close()
|
||||
}
|
||||
|
||||
ps.connection = redis.PubSubConn{Conn: NewClient().GetConn()}
|
||||
for id := range ps.subscriptions {
|
||||
if err := ps.connection.Subscribe(ChannelKey(id)); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
86
pkg/redis/pubsub_test.go
Normal file
86
pkg/redis/pubsub_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/testhelper"
|
||||
)
|
||||
|
||||
func TestInitPubsub(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
var listener Listener
|
||||
listener = NewPubSub()
|
||||
|
||||
if listener == nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestInitPubSub", "PubSub", listener)
|
||||
}
|
||||
|
||||
numSubs := 0
|
||||
if listener.Length() != 0 {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestInitPubSub", 0, numSubs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddSubscription(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
listener := NewPubSub(GetMockPool().Get())
|
||||
|
||||
err := listener.Add("TESTVIN123")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestAddSubscription", nil, err)
|
||||
}
|
||||
|
||||
numSubs := 1
|
||||
if listener.Length() != numSubs {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestAddSubscription", numSubs, listener.Length())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveSubscription(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
listener := NewPubSub(GetMockPool().Get())
|
||||
|
||||
err := listener.Add("TESTVIN123")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestRemoveSubscription", nil, err)
|
||||
}
|
||||
|
||||
numSubs := 1
|
||||
if listener.Length() != numSubs {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestRemoveSubscription", numSubs, listener.Length())
|
||||
}
|
||||
|
||||
err = listener.Remove("TESTVIN123")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestRemoveSubscription", nil, err)
|
||||
}
|
||||
|
||||
numSubs = 0
|
||||
if listener.Length() != numSubs {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestRemoveSubscription", numSubs, listener.Length())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriptionListen(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
listener := NewPubSub(GetMockPool().Get())
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
mockFunc := func(string, []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
go listener.Listen(ctx, mockFunc)
|
||||
}
|
||||
|
||||
func TestSubscriptionRestart(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
listener := NewPubSub(GetMockPool().Get())
|
||||
err := listener.Restart()
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestSubscriptionRestart", nil, err)
|
||||
}
|
||||
}
|
||||
241
pkg/redis/queues.go
Normal file
241
pkg/redis/queues.go
Normal file
@@ -0,0 +1,241 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gomodule/redigo/redis"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"fiskerinc.com/modules/logger"
|
||||
"fiskerinc.com/modules/scheduler"
|
||||
)
|
||||
|
||||
const (
|
||||
blockTimeout = 5
|
||||
reconnectTimeout = 5
|
||||
)
|
||||
|
||||
type Payload struct {
|
||||
channel string
|
||||
data []byte
|
||||
}
|
||||
|
||||
// Queues is a struct used for tracking Redis queues
|
||||
// follows the Listener interface
|
||||
type Queues struct {
|
||||
connection ClientPoolInterface
|
||||
queues Set
|
||||
args redis.Args
|
||||
mu sync.RWMutex
|
||||
retryQueue scheduler.Bucket[Payload]
|
||||
}
|
||||
|
||||
// NewQueues generates a new working Queues object
|
||||
func NewQueues(args ...Pool) *Queues {
|
||||
q := make(Set)
|
||||
var clientPool ClientPoolInterface
|
||||
if len(args) > 0 {
|
||||
clientPool = NewClientPool(args[0])
|
||||
} else {
|
||||
clientPool = NewClientPool()
|
||||
}
|
||||
|
||||
return &Queues{
|
||||
connection: clientPool,
|
||||
queues: q,
|
||||
args: queuesToArgs(q),
|
||||
retryQueue: scheduler.Bucket[Payload]{},
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds queue for listener to block on
|
||||
// follows the format "queue:<id>"
|
||||
func (q *Queues) Add(id string) error {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
ok := q.queues.Add(id)
|
||||
if !ok {
|
||||
return errors.Errorf("%v already in queues", id)
|
||||
}
|
||||
q.args = queuesToArgs(q.queues)
|
||||
logger.At(logger.Debug(), "Queues::Add conn", id).Send()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove queue from listener blocking
|
||||
// follows the format "queue:<id>"
|
||||
func (q *Queues) Remove(id string) error {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
ok := q.queues.Remove(id)
|
||||
if !ok {
|
||||
return errors.Errorf("%v does not exist in queues", id)
|
||||
}
|
||||
q.args = queuesToArgs(q.queues)
|
||||
logger.At(logger.Debug(), "Queues::Remove conn", id).Send()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *Queues) getArgs() redis.Args {
|
||||
q.mu.RLock()
|
||||
defer q.mu.RUnlock()
|
||||
|
||||
return q.args
|
||||
}
|
||||
|
||||
// Listen to redis by blocking on all lists
|
||||
// currently within the set
|
||||
func (q *Queues) Listen(ctx context.Context, handler func(string, []byte) error) error {
|
||||
q.QueryRun(ctx)
|
||||
isListening := true
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
isListening = false
|
||||
break
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}()
|
||||
sampled := logger.Sample(zerolog.LevelSampler{
|
||||
DebugSampler: &zerolog.BurstSampler{
|
||||
Burst: 1,
|
||||
Period: 1 * time.Minute,
|
||||
},
|
||||
})
|
||||
queueMaps := make(map[string][]byte)
|
||||
for isListening {
|
||||
clear(queueMaps)
|
||||
err := q.Process(handler, sampled, queueMaps)
|
||||
if err != nil {
|
||||
done <- err
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListenChannel dumps redis messages to channel rather
|
||||
// than using a traditional handler
|
||||
func (q *Queues) ListenChannel() error {
|
||||
// stub
|
||||
return nil
|
||||
}
|
||||
|
||||
// Length returns number of queues
|
||||
func (q *Queues) Length() int {
|
||||
return len(q.queues)
|
||||
}
|
||||
|
||||
// Restart re-initializes queues connection
|
||||
func (q *Queues) Restart() error {
|
||||
q.connection = NewClientPool()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Queues) timerFunc() {
|
||||
c.retryQueue.Process(func(payload Payload) {
|
||||
logger.Debug().Msgf("QueryRun::closing session %s ", payload.channel)
|
||||
client := c.connection.GetFromPool()
|
||||
// Attempt to stop base64 encoding messages
|
||||
err := client.SafeQueueMessage(payload.channel, string(payload.data))
|
||||
client.Close()
|
||||
if err != nil {
|
||||
logger.At(logger.Error(), payload.channel, "redis").Err(err).Send()
|
||||
} else {
|
||||
logger.Info().Msgf("Unable to send to websocket, added %s back to queue", payload.channel)
|
||||
}
|
||||
})
|
||||
}
|
||||
func (c *Queues) QueryRun(ctx context.Context) {
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
c.timerFunc()
|
||||
case <-ctx.Done():
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
func (q *Queues) retry(key string, payload []byte) {
|
||||
qItem := Payload{
|
||||
channel: key,
|
||||
data: payload,
|
||||
}
|
||||
q.retryQueue.Schedule(qItem)
|
||||
|
||||
}
|
||||
func (q *Queues) Process(handler func(string, []byte) error, sampleLogger zerolog.Logger, queueMaps map[string][]byte) error {
|
||||
args := q.getArgs()
|
||||
sleepTime := blockTimeout * time.Millisecond
|
||||
if len(args) == 0 {
|
||||
sampleLogger.Debug().Msgf("Queue:Process no args")
|
||||
time.Sleep(sleepTime)
|
||||
return nil
|
||||
}
|
||||
|
||||
poppedMap, err := q.lPop(args, sampleLogger, queueMaps)
|
||||
if err != nil && len(poppedMap) == 0 {
|
||||
if err != nil {
|
||||
sampleLogger.Debug().Msgf("Queue:Process lPop failed")
|
||||
logger.At(logger.Error(), "Queue:Process", "redis:unable to LPop").Err(err).Send()
|
||||
}
|
||||
time.Sleep(sleepTime)
|
||||
return nil
|
||||
}
|
||||
for channel, payload := range poppedMap {
|
||||
logger.Debug().Msgf("call handler, key- %s, data- %v", channel, string(payload))
|
||||
if err = handler(channel, payload); err != nil {
|
||||
logger.At(logger.Error(), channel, "gateway:unable to send: retry in 3 sec").Err(err).Send()
|
||||
q.retry(channel, payload)
|
||||
} else {
|
||||
logger.At(logger.Debug(), channel, "redis").
|
||||
Str("Gateway:msg", string(payload)).
|
||||
Msgf("VIN %s received queued msg ", channel)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *Queues) lPop(args redis.Args, sampleLogger zerolog.Logger, queueMaps map[string][]byte) (map[string][]byte, error) {
|
||||
client := q.connection.GetFromPool()
|
||||
defer client.Close()
|
||||
for _, arg := range args {
|
||||
reply, err := redis.Bytes(client.GetConn().Do("LPOP", arg))
|
||||
if err != nil {
|
||||
if errors.Is(err, redis.ErrNil) {
|
||||
sampleLogger.Debug().Msgf("LPop::null value returned by redis")
|
||||
|
||||
continue
|
||||
}
|
||||
logger.At(logger.Error(), "LPop", "redis").
|
||||
Str("LPop", "").Err(err).Send()
|
||||
|
||||
return queueMaps, err
|
||||
}
|
||||
logger.Debug().Msgf("LPop: vin %s reply %d ", arg.(string), len(reply))
|
||||
queueMaps[ParseQueueKey(arg.(string))] = reply
|
||||
}
|
||||
return queueMaps, nil
|
||||
}
|
||||
|
||||
func queuesToArgs(s Set) redis.Args {
|
||||
a := make(redis.Args, len(s))
|
||||
i := 0
|
||||
for e := range s {
|
||||
a[i] = QueueKey(e)
|
||||
i++
|
||||
}
|
||||
return a
|
||||
}
|
||||
131
pkg/redis/queues_test.go
Normal file
131
pkg/redis/queues_test.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"fiskerinc.com/modules/testhelper"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestInitQueues(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
var q Listener
|
||||
q = NewQueues()
|
||||
|
||||
numQueues := 0
|
||||
if q.Length() != numQueues {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestInitQueues", numQueues, q.Length())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddQueue(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
var q Listener
|
||||
q = NewQueues()
|
||||
|
||||
err := q.Add("TESTVIN123")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestAddQueue", nil, err)
|
||||
}
|
||||
|
||||
numQueues := 1
|
||||
if q.Length() != numQueues {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestAddQueue", numQueues, q.Length())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveQueue(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
var q Listener
|
||||
q = NewQueues()
|
||||
|
||||
err := q.Add("TESTVIN123")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestRemoveQueue", nil, err)
|
||||
}
|
||||
|
||||
numQueues := 1
|
||||
if q.Length() != numQueues {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestRemoveQueue", numQueues, q.Length())
|
||||
}
|
||||
|
||||
err = q.Remove("TESTVIN123")
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestRemoveQueue", nil, err)
|
||||
}
|
||||
|
||||
numQueues = 0
|
||||
if q.Length() != numQueues {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestRemoveQueue", numQueues, q.Length())
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueuesListener(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
q := NewQueues()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
mockFunc := func(string, []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
go q.Listen(ctx, mockFunc)
|
||||
}
|
||||
|
||||
func TestQueuesRestart(t *testing.T) {
|
||||
MockRedisConnection()
|
||||
q := NewQueues()
|
||||
err := q.Restart()
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestQueuesRestart", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueuesThreaded(t *testing.T) {
|
||||
t.Skip() //remove this for local testing or testing with redis
|
||||
UpdateRedisConnection("localhost", "6379", "fisker123")
|
||||
os.Setenv("REDIS_MAXACTIVECONN", "100")
|
||||
counter := 0
|
||||
q := NewQueues()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
var wg sync.WaitGroup
|
||||
|
||||
go q.Listen(ctx, func(s string, b []byte) error {
|
||||
counter++
|
||||
t.Logf("received call from channel %v, counter %d data %v", s, counter, string(b))
|
||||
|
||||
if counter%2 != 0 {
|
||||
return errors.New("fake error")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
q.Add("TESTVIN123")
|
||||
|
||||
for i := 0; i < 95; i++ {
|
||||
wg.Add(1)
|
||||
go func(i int) {
|
||||
conn := q.connection.GetFromPool()
|
||||
err := conn.SafeQueueMessage("TESTVIN123", fmt.Sprintf("hello fisker! %d", i))
|
||||
conn.Close()
|
||||
if err != nil {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestConnQueueMessage", nil, err)
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
wg.Done()
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
t.Log("success")
|
||||
time.Sleep(25 * time.Second)
|
||||
t.Fail()
|
||||
|
||||
}
|
||||
86
pkg/redis/redisutils/cache_set.go
Normal file
86
pkg/redis/redisutils/cache_set.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package redisutils
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"fiskerinc.com/modules/redis"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCacheDoesntExist = errors.New("key doesn't exist")
|
||||
ErrWrongResponseFormat = errors.New("wrong response format")
|
||||
)
|
||||
|
||||
type CachedSet struct {
|
||||
redis redis.Client
|
||||
}
|
||||
|
||||
func (s *CachedSet) SetConnection(client redis.Client) {
|
||||
s.redis = client
|
||||
}
|
||||
|
||||
func (s *CachedSet) GetCachedSet(key string) (map[string]struct{}, error) {
|
||||
batch := redis.NewRedisBatchCommands()
|
||||
batch.Add("EXISTS", key)
|
||||
batch.Add("SMEMBERS", key)
|
||||
|
||||
resultsI, err := s.redis.ExecuteBatch(batch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results, ok := resultsI.([]interface{})
|
||||
if !ok || len(results) != 2 {
|
||||
return nil, errResponseFormat("[2]interface{}", resultsI)
|
||||
}
|
||||
|
||||
keyExists, ok := results[0].(int64)
|
||||
if !ok {
|
||||
return nil, errResponseFormat("int64", results[0])
|
||||
}
|
||||
if keyExists == 0 {
|
||||
return nil, ErrCacheDoesntExist
|
||||
}
|
||||
|
||||
cacheRes, ok := results[1].([]interface{})
|
||||
if !ok {
|
||||
return nil, errResponseFormat("[]interface{}", results[1])
|
||||
}
|
||||
|
||||
cacheSet := make(map[string]struct{}, len(cacheRes))
|
||||
for _, signal := range cacheRes {
|
||||
s, ok := signal.([]byte)
|
||||
if !ok {
|
||||
return nil, errResponseFormat("[]byte", signal)
|
||||
}
|
||||
cacheSet[string(s)] = struct{}{}
|
||||
}
|
||||
|
||||
return cacheSet, nil
|
||||
}
|
||||
|
||||
func (s *CachedSet) UpdateSetCache(key string, cacheValues []interface{}) error {
|
||||
saddCommand := append([]interface{}{"SADD", key}, cacheValues...)
|
||||
_, err := s.redis.Execute(saddCommand...)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *CachedSet) CreateSetCache(key string, cacheValues []interface{}, expireTime time.Time) error {
|
||||
batch := redis.NewRedisBatchCommands()
|
||||
|
||||
saddCommand := append([]interface{}{"SADD", key}, cacheValues...)
|
||||
batch.Add(saddCommand...)
|
||||
batch.Add("EXPIREAT", key, expireTime.Unix())
|
||||
|
||||
_, err := s.redis.ExecuteBatch(batch)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func errResponseFormat(expectedType string, got interface{}) error {
|
||||
return errors.WithStack(
|
||||
errors.WithMessagef(ErrWrongResponseFormat, "expected type: %s, got: %v", expectedType, got),
|
||||
)
|
||||
}
|
||||
28
pkg/redis/redisutils/cache_set_mock.go
Normal file
28
pkg/redis/redisutils/cache_set_mock.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package redisutils
|
||||
|
||||
import (
|
||||
"fiskerinc.com/modules/redis"
|
||||
"time"
|
||||
)
|
||||
|
||||
type GetCachedSetMock func(key string) (map[string]struct{}, error)
|
||||
|
||||
type CacheSetMock struct {
|
||||
GetCachedSetMock func(key string) (map[string]struct{}, error)
|
||||
UpdateSetCacheMock func(key string, cacheValues []interface{}) error
|
||||
CreateSetCacheMock func(key string, cacheValues []interface{}, expireTime time.Time) error
|
||||
}
|
||||
|
||||
func (c CacheSetMock) SetConnection(client redis.Client) {}
|
||||
|
||||
func (c CacheSetMock) GetCachedSet(key string) (map[string]struct{}, error) {
|
||||
return c.GetCachedSetMock(key)
|
||||
}
|
||||
|
||||
func (c CacheSetMock) UpdateSetCache(key string, cacheValues []interface{}) error {
|
||||
return c.UpdateSetCacheMock(key, cacheValues)
|
||||
}
|
||||
|
||||
func (c CacheSetMock) CreateSetCache(key string, cacheValues []interface{}, expireTime time.Time) error {
|
||||
return c.CreateSetCacheMock(key, cacheValues, expireTime)
|
||||
}
|
||||
59
pkg/redis/redisutils/cache_set_mock_funcs.go
Normal file
59
pkg/redis/redisutils/cache_set_mock_funcs.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package redisutils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrFailedFunc = errors.New("some error")
|
||||
|
||||
// GetCachedSetMock funcs.
|
||||
|
||||
func SuccessGetCachedSetMock(t *testing.T, expKey string, response map[string]struct{}) func(key string) (map[string]struct{}, error) {
|
||||
return func(key string) (map[string]struct{}, error) {
|
||||
assert.Equal(t, expKey, key)
|
||||
|
||||
return response, nil
|
||||
}
|
||||
}
|
||||
|
||||
func NoKeyGetCachedSetMock(key string) (map[string]struct{}, error) {
|
||||
return nil, ErrCacheDoesntExist
|
||||
}
|
||||
|
||||
func FailedGetCachedSetMock(key string) (map[string]struct{}, error) {
|
||||
return nil, ErrFailedFunc
|
||||
}
|
||||
|
||||
// UpdateSetCacheMock funcs.
|
||||
|
||||
func SuccessUpdateSetCacheMock(t *testing.T, expKey string, expCacheValues []interface{}) func(key string, cacheValues []interface{}) error {
|
||||
return func(key string, cacheValues []interface{}) error {
|
||||
assert.Equal(t, expKey, key)
|
||||
assert.Equal(t, expCacheValues, cacheValues)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func FailUpdateSetCacheMock(key string, cacheValues []interface{}) error {
|
||||
return ErrFailedFunc
|
||||
}
|
||||
|
||||
// CreateSetCacheMock funcs.
|
||||
|
||||
func SuccessCreateSetCacheMock(t *testing.T, expKey string, expCacheValues []interface{}, expExpireTime time.Time) func(key string, cacheValues []interface{}, expireTime time.Time) error {
|
||||
return func(key string, cacheValues []interface{}, expireTime time.Time) error {
|
||||
assert.Equal(t, expKey, key)
|
||||
assert.Equal(t, expCacheValues, cacheValues)
|
||||
assert.Equal(t, expExpireTime, expireTime)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func FailCreateSetCacheMock(key string, cacheValues []interface{}, expireTime time.Time) error {
|
||||
return ErrFailedFunc
|
||||
}
|
||||
211
pkg/redis/redisutils/cache_set_test.go
Normal file
211
pkg/redis/redisutils/cache_set_test.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package redisutils_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"fiskerinc.com/modules/redis/redisutils"
|
||||
"fiskerinc.com/modules/redis/tester"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetCacheSet(t *testing.T) {
|
||||
key := "someKey"
|
||||
redisMock := tester.NewRedisMock()
|
||||
|
||||
tests := map[string]struct {
|
||||
getResults map[string]map[string]interface{}
|
||||
batchError error
|
||||
expRes map[string]struct{}
|
||||
expError error
|
||||
}{
|
||||
"correct": {
|
||||
getResults: map[string]map[string]interface{}{
|
||||
"EXISTS": {
|
||||
key: int64(1),
|
||||
},
|
||||
"SMEMBERS": {
|
||||
key: []interface{}{
|
||||
[]byte("609:ESP_ActvSig_DTC"),
|
||||
[]byte("832:ADAS_FltIndcr"),
|
||||
[]byte("832:ADAS_IntegtCrsFltTxt"),
|
||||
},
|
||||
},
|
||||
},
|
||||
expRes: map[string]struct{}{
|
||||
"609:ESP_ActvSig_DTC": {},
|
||||
"832:ADAS_FltIndcr": {},
|
||||
"832:ADAS_IntegtCrsFltTxt": {},
|
||||
},
|
||||
},
|
||||
"key_doesnt_exist": {
|
||||
getResults: map[string]map[string]interface{}{
|
||||
"EXISTS": {
|
||||
key: int64(0),
|
||||
},
|
||||
},
|
||||
expError: redisutils.ErrCacheDoesntExist,
|
||||
},
|
||||
"batch_error": {
|
||||
batchError: errors.New("some error"),
|
||||
expError: errors.New("some error"),
|
||||
},
|
||||
"wrong_batch_first_elem": {
|
||||
getResults: map[string]map[string]interface{}{
|
||||
"EXISTS": {
|
||||
key: 1,
|
||||
},
|
||||
"SMEMBERS": {
|
||||
key: []interface{}{},
|
||||
},
|
||||
},
|
||||
expError: errors.New("expected type: int64, got: 1: wrong response format"),
|
||||
},
|
||||
"wrong_batch_second_elem": {
|
||||
getResults: map[string]map[string]interface{}{
|
||||
"EXISTS": {
|
||||
key: int64(1),
|
||||
},
|
||||
"SMEMBERS": {
|
||||
key: 1,
|
||||
},
|
||||
},
|
||||
expError: errors.New("expected type: []interface{}, got: 1: wrong response format"),
|
||||
},
|
||||
"wrong_batch_second_sub_elem": {
|
||||
getResults: map[string]map[string]interface{}{
|
||||
"EXISTS": {
|
||||
key: int64(1),
|
||||
},
|
||||
"SMEMBERS": {
|
||||
key: []interface{}{1},
|
||||
},
|
||||
},
|
||||
expError: errors.New("expected type: []byte, got: 1: wrong response format"),
|
||||
},
|
||||
}
|
||||
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
redisMock.GetCommandResult = tt.getResults
|
||||
redisMock.Error = tt.batchError
|
||||
|
||||
cSet := redisutils.CachedSet{}
|
||||
cSet.SetConnection(redisMock)
|
||||
|
||||
res, err := cSet.GetCachedSet(key)
|
||||
if err != nil && tt.expError != nil {
|
||||
assert.Equal(t, tt.expError.Error(), err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.expError, err)
|
||||
assert.Equal(t, tt.expRes, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateSetCache(t *testing.T) {
|
||||
redisMock := tester.NewRedisMock()
|
||||
mockTime := time.Date(2022, 8, 11, 15, 53, 0, 0, time.UTC)
|
||||
|
||||
tests := map[string]struct {
|
||||
cacheValues []interface{}
|
||||
batchError error
|
||||
expSetValues map[string]tester.ExpiringCache
|
||||
expError error
|
||||
}{
|
||||
"correct": {
|
||||
cacheValues: []interface{}{
|
||||
"609:ESP_ActvSig_DTC",
|
||||
"832:ADAS_FltIndcr",
|
||||
"832:ADAS_IntegtCrsFltTxt",
|
||||
},
|
||||
expSetValues: map[string]tester.ExpiringCache{
|
||||
"someKey": {
|
||||
Value: []interface{}{
|
||||
"609:ESP_ActvSig_DTC",
|
||||
"832:ADAS_FltIndcr",
|
||||
"832:ADAS_IntegtCrsFltTxt",
|
||||
},
|
||||
Expires: 1660233180,
|
||||
},
|
||||
},
|
||||
},
|
||||
"fail": {
|
||||
batchError: errors.New("some error"),
|
||||
expError: errors.New("some error"),
|
||||
},
|
||||
}
|
||||
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
redisMock.Reset()
|
||||
redisMock.Error = tt.batchError
|
||||
|
||||
cSet := redisutils.CachedSet{}
|
||||
cSet.SetConnection(redisMock)
|
||||
|
||||
err := cSet.CreateSetCache("someKey", tt.cacheValues, mockTime)
|
||||
if err != nil && tt.expError != nil {
|
||||
assert.Equal(t, tt.expError.Error(), err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.expError, err)
|
||||
assert.Equal(t, tt.expSetValues, redisMock.SetValues)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateSetCache(t *testing.T) {
|
||||
redisMock := tester.NewRedisMock()
|
||||
|
||||
tests := map[string]struct {
|
||||
cacheValues []interface{}
|
||||
execError error
|
||||
expSetValues map[string]tester.ExpiringCache
|
||||
expError error
|
||||
}{
|
||||
"correct": {
|
||||
cacheValues: []interface{}{
|
||||
"609:ESP_ActvSig_DTC",
|
||||
"832:ADAS_FltIndcr",
|
||||
"832:ADAS_IntegtCrsFltTxt",
|
||||
},
|
||||
expSetValues: map[string]tester.ExpiringCache{
|
||||
"someKey": {
|
||||
Value: []interface{}{
|
||||
"609:ESP_ActvSig_DTC",
|
||||
"832:ADAS_FltIndcr",
|
||||
"832:ADAS_IntegtCrsFltTxt",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"fail": {
|
||||
execError: errors.New("some error"),
|
||||
expError: errors.New("some error"),
|
||||
},
|
||||
}
|
||||
|
||||
for name, tt := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
redisMock.Reset()
|
||||
redisMock.Error = tt.execError
|
||||
|
||||
cSet := redisutils.CachedSet{}
|
||||
cSet.SetConnection(redisMock)
|
||||
|
||||
err := cSet.UpdateSetCache("someKey", tt.cacheValues)
|
||||
if err != nil && tt.expError != nil {
|
||||
assert.Equal(t, tt.expError.Error(), err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
assert.Equal(t, tt.expError, err)
|
||||
assert.Equal(t, tt.expSetValues, redisMock.SetValues)
|
||||
})
|
||||
}
|
||||
}
|
||||
32
pkg/redis/redisutils/check_set.go
Normal file
32
pkg/redis/redisutils/check_set.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package redisutils
|
||||
|
||||
import (
|
||||
re "fiskerinc.com/modules/redis"
|
||||
"github.com/gomodule/redigo/redis"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func CheckSet(conn re.Client, id string, keys interface{}) ([]bool, error) {
|
||||
ckeys, ok := keys.([]string)
|
||||
if !ok {
|
||||
return nil, errors.New("keys is not []string")
|
||||
}
|
||||
|
||||
results := make([]bool, len(ckeys))
|
||||
batch := re.NewRedisBatchCommands()
|
||||
|
||||
for _, key := range ckeys {
|
||||
batch.Add("SISMEMBER", id, key)
|
||||
}
|
||||
|
||||
values, err := conn.ExecuteBatch(batch)
|
||||
if err != nil {
|
||||
return results, err
|
||||
}
|
||||
|
||||
err = redis.ScanSlice(values.([]interface{}), &results)
|
||||
if err != nil {
|
||||
return results, errors.WithStack(err)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
25
pkg/redis/set.go
Normal file
25
pkg/redis/set.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package redis
|
||||
|
||||
// Set represents a set object using a map of empty structs
|
||||
type Set map[string]void
|
||||
type void struct{}
|
||||
|
||||
var member void
|
||||
|
||||
// Add id to the set, returns false if id exists
|
||||
func (s Set) Add(id string) bool {
|
||||
if _, ok := s[id]; ok {
|
||||
return false
|
||||
}
|
||||
s[id] = member
|
||||
return true
|
||||
}
|
||||
|
||||
// Remove id from the set, return false if id doesn't exist
|
||||
func (s Set) Remove(id string) bool {
|
||||
if _, ok := s[id]; !ok {
|
||||
return false
|
||||
}
|
||||
delete(s, id)
|
||||
return true
|
||||
}
|
||||
70
pkg/redis/set_test.go
Normal file
70
pkg/redis/set_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/testhelper"
|
||||
)
|
||||
|
||||
func TestInitSet(t *testing.T) {
|
||||
s := make(Set)
|
||||
|
||||
numS := 0
|
||||
if len(s) != numS {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestInitSet", numS, len(s))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetAdd(t *testing.T) {
|
||||
s := make(Set)
|
||||
|
||||
if ok := s.Add("TESTVIN123"); !ok {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestSetAdd", true, ok)
|
||||
}
|
||||
|
||||
numS := 1
|
||||
if len(s) != numS {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestInitSet", numS, len(s))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetAddError(t *testing.T) {
|
||||
s := make(Set)
|
||||
|
||||
s.Add("TESTVIN123")
|
||||
if ok := s.Add("TESTVIN123"); ok {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestSetAddError", false, ok)
|
||||
}
|
||||
|
||||
numS := 1
|
||||
if len(s) != numS {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestInitSet", numS, len(s))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetRemove(t *testing.T) {
|
||||
s := make(Set)
|
||||
|
||||
s.Add("TESTVIN123")
|
||||
if ok := s.Remove("TESTVIN123"); !ok {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestSetRemove", true, ok)
|
||||
}
|
||||
|
||||
numS := 0
|
||||
if len(s) != numS {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestSetRemove", numS, len(s))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetRemoveError(t *testing.T) {
|
||||
s := make(Set)
|
||||
|
||||
if ok := s.Remove("TESTVIN123"); ok {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestSetRemove", false, ok)
|
||||
}
|
||||
|
||||
numS := 0
|
||||
if len(s) != numS {
|
||||
t.Errorf(testhelper.TestErrorTemplate, "TestSetRemove", numS, len(s))
|
||||
}
|
||||
}
|
||||
29
pkg/redis/tester/expiring_cache.go
Normal file
29
pkg/redis/tester/expiring_cache.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package tester
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type ExpiringCache struct {
|
||||
Value interface{}
|
||||
Expires int
|
||||
}
|
||||
|
||||
// get string value for comparison
|
||||
func (e *ExpiringCache) StringValue() (string, error) {
|
||||
switch e.Value.(type) {
|
||||
case string:
|
||||
return e.Value.(string), nil
|
||||
default:
|
||||
data, err := json.Marshal(&e.Value)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ExpiringCache) String() string {
|
||||
return fmt.Sprintf("%s, expires %d", e.Value, e.Expires)
|
||||
}
|
||||
662
pkg/redis/tester/mock_client.go
Normal file
662
pkg/redis/tester/mock_client.go
Normal file
@@ -0,0 +1,662 @@
|
||||
package tester
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"fiskerinc.com/modules/common"
|
||||
"fiskerinc.com/modules/redis"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
errInsufficientArgs = "insufficient number of args"
|
||||
errBadSetValue = "bad set value"
|
||||
errBadExpireValue = "bad expire value"
|
||||
errExpectedKey = "expected key to be string"
|
||||
errExpectedMessage = "expected message to be []byte"
|
||||
)
|
||||
|
||||
func NewRedisMock() *MockRedis {
|
||||
redis.MockRedisConnection()
|
||||
conn := redis.GetMockPool().Get()
|
||||
mockRedis := &MockRedis{}
|
||||
mockRedis.SetConn(conn)
|
||||
mockRedis.Reset()
|
||||
|
||||
return mockRedis
|
||||
}
|
||||
|
||||
type MockRedis struct {
|
||||
redis.Connection
|
||||
mu sync.Mutex
|
||||
// HGET results for key and field
|
||||
HGETResults map[string]map[string]interface{}
|
||||
// HGETALL results array for key
|
||||
HGETALLResults map[string][]interface{}
|
||||
// SMSMEMBER results for key and member
|
||||
SISMEMBEResults map[string]map[string]interface{}
|
||||
// Results for get commands (GET, EXISTS, SMEMBERS) and key
|
||||
GetCommandResult map[string]map[string]interface{}
|
||||
ExecuteResults interface{}
|
||||
GetResults interface{}
|
||||
GetCacheResults string
|
||||
GetSetResults string
|
||||
RetrieveResult string
|
||||
Error error
|
||||
PublishedMessages map[string]interface{}
|
||||
GetObjectResults map[string]string
|
||||
GetObjectRawResults map[string][]byte
|
||||
GetMultiResults []interface{}
|
||||
SetValues map[string]ExpiringCache
|
||||
ExecutedCommands []interface{}
|
||||
Closed bool
|
||||
}
|
||||
|
||||
func (m *MockRedis) Delete(id ...interface{}) error {
|
||||
return m.processDelCommand(append([]interface{}{"DEL"}, id...))
|
||||
}
|
||||
|
||||
func (m *MockRedis) Close() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.Closed = true
|
||||
return m.Error
|
||||
}
|
||||
|
||||
func (m *MockRedis) Execute(command ...interface{}) (interface{}, error) {
|
||||
_, _ = m.executeBatch(&redis.RedisBatchCommands{Commands: [][]interface{}{command}})
|
||||
|
||||
return m.ExecuteResults, m.Error
|
||||
}
|
||||
|
||||
func (m *MockRedis) ExecuteBatch(batch *redis.RedisBatchCommands) (interface{}, error) {
|
||||
if m.Error != nil {
|
||||
return nil, m.Error
|
||||
}
|
||||
if batch.IsEmpty() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return m.executeBatch(batch)
|
||||
}
|
||||
func (c *MockRedis) SafeQueueMessage(id string, message interface{}) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
return c.QueueMessage(id, message)
|
||||
}
|
||||
|
||||
// SafeQueueMessage is the thread-safe implementation of QueueMessage
|
||||
func (c *MockRedis) SafePublishMessage(id string, message interface{}) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
return c.PublishMessage(id, message)
|
||||
}
|
||||
|
||||
func (m *MockRedis) SetObjectField(key, field string, value interface{}) error {
|
||||
return m.processHSetCommand([]interface{}{"HSET", key, field, value})
|
||||
}
|
||||
|
||||
func (m *MockRedis) GetObjectField(string, string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (m *MockRedis) SafeExecuteBatch(batch *redis.RedisBatchCommands) (interface{}, error) {
|
||||
if m.Error != nil {
|
||||
return nil, m.Error
|
||||
}
|
||||
if batch.IsEmpty() {
|
||||
return nil, nil
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
return m.executeBatch(batch)
|
||||
}
|
||||
|
||||
func (m *MockRedis) executeBatch(batch *redis.RedisBatchCommands) (interface{}, error) {
|
||||
if m.Error != nil {
|
||||
return nil, m.Error
|
||||
}
|
||||
|
||||
var err error
|
||||
var val interface{}
|
||||
var vals []interface{}
|
||||
|
||||
results := []interface{}{}
|
||||
|
||||
defer batch.Clear()
|
||||
|
||||
for _, command := range batch.Commands {
|
||||
m.ExecutedCommands = append(m.ExecutedCommands, command)
|
||||
val = int64(0)
|
||||
|
||||
switch command[0] {
|
||||
case "DEL":
|
||||
err = m.processDelCommand(command)
|
||||
case "GET", "EXISTS", "SMEMBERS":
|
||||
val, err = m.processGetCommand(command)
|
||||
case "EXPIRE", "EXPIREAT":
|
||||
err = m.processExpireCommand(command)
|
||||
case "HGETALL":
|
||||
vals, err = m.processHGetAllCommand(command)
|
||||
if err == nil {
|
||||
results = append(results, vals)
|
||||
continue
|
||||
}
|
||||
case "HGET":
|
||||
val, err = m.processHGetCommand(command)
|
||||
case "HSET":
|
||||
err = m.processHSetCommand(command)
|
||||
case "PUBLISH":
|
||||
err = m.processPublishCommand(command)
|
||||
case "SET":
|
||||
err = m.processSetCommand(command)
|
||||
case "RPUSH":
|
||||
err = m.processQueueCommand(command)
|
||||
case "SISMEMBER":
|
||||
val, err = m.processSISMemberCommand(command)
|
||||
case "SADD":
|
||||
err = m.processSADDCommand(command)
|
||||
}
|
||||
if err == nil {
|
||||
results = append(results, val)
|
||||
}
|
||||
}
|
||||
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (m *MockRedis) processGetCommand(command []interface{}) (interface{}, error) {
|
||||
if len(command) != 2 {
|
||||
return nil, errors.New(errInsufficientArgs)
|
||||
}
|
||||
|
||||
return m.getMapMapResult(m.GetCommandResult, command), nil
|
||||
}
|
||||
|
||||
func (m *MockRedis) processSetCommand(command []interface{}) error {
|
||||
cache := ExpiringCache{}
|
||||
|
||||
if len(command) < 3 {
|
||||
return errors.New(errInsufficientArgs)
|
||||
} else {
|
||||
data, ok := command[2].([]byte)
|
||||
if !ok {
|
||||
return errors.New(errBadSetValue)
|
||||
}
|
||||
cache.Value = string(data)
|
||||
}
|
||||
|
||||
if len(command) == 5 && command[3] == "EX" {
|
||||
expires, ok := command[4].(int)
|
||||
if !ok {
|
||||
return errors.New(errBadExpireValue)
|
||||
}
|
||||
cache.Expires = expires
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%v", command[1])
|
||||
m.SetValues[key] = cache
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRedis) getMapMapResult(mmap map[string]map[string]interface{}, command []interface{}) interface{} {
|
||||
if mmap == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
value, ok := mmap[command[0].(string)]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, ok := value[command[1].(string)]
|
||||
if ok {
|
||||
return result
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRedis) processHGetCommand(command []interface{}) (interface{}, error) {
|
||||
if len(command) != 3 {
|
||||
return nil, errors.New("HGET incorrect number of parameters")
|
||||
}
|
||||
|
||||
return m.getMapMapResult(m.HGETResults, command[1:]), nil
|
||||
}
|
||||
|
||||
func (m *MockRedis) processHSetCommand(command []interface{}) error {
|
||||
cache := ExpiringCache{}
|
||||
numArgs := len(command)
|
||||
|
||||
if numArgs < 4 || numArgs%2 != 0 {
|
||||
return errors.New(errInsufficientArgs)
|
||||
} else {
|
||||
obj, expire := m.getValueCache(command[1].(string))
|
||||
|
||||
for i, value := range command[2:] {
|
||||
if i%2 == 0 {
|
||||
key, ok := value.(string)
|
||||
if !ok {
|
||||
return errors.New(errExpectedKey)
|
||||
}
|
||||
obj[key] = command[i+3]
|
||||
}
|
||||
}
|
||||
|
||||
data, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cache.Value = string(data)
|
||||
cache.Expires = expire
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%v", command[1])
|
||||
m.SetValues[key] = cache
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRedis) getValueCache(key string) (map[string]interface{}, int) {
|
||||
obj := map[string]interface{}{}
|
||||
if cache, ok := m.SetValues[key]; ok {
|
||||
data := cache.Value.(string)
|
||||
err := json.Unmarshal([]byte(data), &obj)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("getValueCache %s %v", key, err))
|
||||
}
|
||||
return obj, cache.Expires
|
||||
}
|
||||
|
||||
return obj, 0
|
||||
}
|
||||
|
||||
func (m *MockRedis) processExpireCommand(command []interface{}) error {
|
||||
if len(command) != 3 {
|
||||
return errors.New(errInsufficientArgs)
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%v", command[1])
|
||||
cache, ok := m.SetValues[key]
|
||||
if ok {
|
||||
expires, err := strconv.Atoi(fmt.Sprint(command[2]))
|
||||
if err == nil {
|
||||
cache.Expires = expires
|
||||
m.SetValues[key] = cache
|
||||
} else {
|
||||
return errors.New(errBadExpireValue)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRedis) processSISMemberCommand(command []interface{}) (interface{}, error) {
|
||||
if len(command) != 3 {
|
||||
return nil, errors.New(errInsufficientArgs)
|
||||
}
|
||||
|
||||
return m.getMapMapResult(m.SISMEMBEResults, command[1:]), nil
|
||||
}
|
||||
|
||||
func (m *MockRedis) processHGetAllCommand(command []interface{}) ([]interface{}, error) {
|
||||
if len(command) != 2 {
|
||||
return nil, errors.New(errInsufficientArgs)
|
||||
}
|
||||
|
||||
if m.HGETALLResults == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
values, ok := m.HGETALLResults[command[1].(string)]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func (m *MockRedis) processQueueCommand(command []interface{}) error {
|
||||
if len(command) != 3 {
|
||||
return errors.New(errInsufficientArgs)
|
||||
}
|
||||
|
||||
return m.QueueMessage(command[1].(string), command[2])
|
||||
}
|
||||
|
||||
func (m *MockRedis) processPublishCommand(command []interface{}) error {
|
||||
if len(command) != 3 {
|
||||
return errors.New(errInsufficientArgs)
|
||||
}
|
||||
|
||||
// Publish message is passed in as []byte, but mock PublishMessage expects object
|
||||
data, ok := command[2].([]byte)
|
||||
if !ok {
|
||||
return errors.New(errExpectedMessage)
|
||||
}
|
||||
msg := map[string]interface{}{}
|
||||
err := json.Unmarshal(data, &msg)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return m.PublishMessage(command[1].(string), msg)
|
||||
}
|
||||
|
||||
func (m *MockRedis) processDelCommand(command []interface{}) error {
|
||||
if len(command) != 2 {
|
||||
return errors.New(errInsufficientArgs)
|
||||
}
|
||||
|
||||
key := command[1].(string)
|
||||
cache := ExpiringCache{Value: "DELETED"}
|
||||
m.SetValues[key] = cache
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRedis) processSADDCommand(command []interface{}) error {
|
||||
key := command[1].(string)
|
||||
cache := ExpiringCache{Value: command[2:]}
|
||||
m.SetValues[key] = cache
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRedis) Get(string) (interface{}, error) {
|
||||
return m.GetResults, m.Error
|
||||
}
|
||||
|
||||
func (m *MockRedis) GetCache(id string, data interface{}, expire int) error {
|
||||
err := json.Unmarshal([]byte(m.GetCacheResults), data)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRedis) GetSet(id string, data interface{}) error {
|
||||
if m.Error != nil {
|
||||
return m.Error
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(m.GetSetResults), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRedis) PublishMessage(id string, msg interface{}) error {
|
||||
if m.Error != nil {
|
||||
return m.Error
|
||||
}
|
||||
|
||||
if m.PublishedMessages == nil {
|
||||
m.PublishedMessages = map[string]interface{}{}
|
||||
}
|
||||
|
||||
// In the real thing, you message is converted to JSON and sent out, so further changes
|
||||
// to the struct do not change redis, but because we are keeping the struct, any internal pointer
|
||||
// can still be affected
|
||||
msg, _ = BeginDeepCopy(msg)
|
||||
|
||||
// trim prefix for publishing and queueing of message
|
||||
key := strings.Replace(strings.Replace(id, "channel:", "", 1), "queue:", "", 1)
|
||||
|
||||
m.PublishedMessages[key] = msg
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRedis) QueueMessage(id string, msg interface{}) error {
|
||||
return m.PublishMessage(id, msg)
|
||||
}
|
||||
|
||||
func (m *MockRedis) BatchPublishMessages(ids []string, messages []interface{}) error {
|
||||
if len(ids) != len(messages) {
|
||||
return errors.Errorf(
|
||||
"mismatch number of ids and messages. have %d ids and %d messages",
|
||||
len(ids),
|
||||
len(messages),
|
||||
)
|
||||
}
|
||||
|
||||
for i := 0; i < len(ids); i++ {
|
||||
err := m.SafePublishMessage(ids[i], messages[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRedis) BatchQueueMessages(ids []string, messages []interface{}) error {
|
||||
if len(ids) != len(messages) {
|
||||
return errors.Errorf(
|
||||
"mismatch number of ids and messages. have %d ids and %d messages",
|
||||
len(ids),
|
||||
len(messages),
|
||||
)
|
||||
}
|
||||
|
||||
for i := 0; i < len(ids); i++ {
|
||||
err := m.QueueMessage(ids[i], messages[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRedis) Retrieve(id string, data interface{}) error {
|
||||
if m.Error != nil {
|
||||
return m.Error
|
||||
}
|
||||
|
||||
err := json.Unmarshal([]byte(m.RetrieveResult), data)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRedis) Set(key string, value interface{}) error {
|
||||
if m.Error != nil {
|
||||
return m.Error
|
||||
}
|
||||
|
||||
m.SetValues[key] = ExpiringCache{
|
||||
Value: value,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRedis) SetCache(key string, value interface{}, expires int) error {
|
||||
if m.Error != nil {
|
||||
return m.Error
|
||||
}
|
||||
|
||||
m.SetValues[key] = ExpiringCache{Value: value, Expires: expires}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRedis) GetObject(key string, obj interface{}) error {
|
||||
if m.Error != nil {
|
||||
return m.Error
|
||||
}
|
||||
|
||||
data, ok := m.GetObjectResults[key]
|
||||
if !ok {
|
||||
return redis.ErrNilObject
|
||||
}
|
||||
err := json.Unmarshal([]byte(data), obj)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRedis) SetObject(key string, value interface{}, expires int) error {
|
||||
return m.SetCache(key, value, expires)
|
||||
}
|
||||
|
||||
func (m *MockRedis) GetMulti(ids []string) ([]interface{}, error) {
|
||||
return m.GetMultiResults, m.Error
|
||||
}
|
||||
|
||||
// Test helper methods
|
||||
|
||||
func (m *MockRedis) HasMessage(id string, msg string) (string, bool) {
|
||||
var compare string
|
||||
|
||||
if value, ok := m.PublishedMessages[id]; ok {
|
||||
if compare, ok = m.isByteSlice(value); !ok {
|
||||
if compare, ok = m.getJSON(value); !ok {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
if compare == msg {
|
||||
return compare, true
|
||||
}
|
||||
}
|
||||
|
||||
return compare, false
|
||||
}
|
||||
|
||||
func (m *MockRedis) getJSON(value interface{}) (string, bool) {
|
||||
result, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return string(result), true
|
||||
}
|
||||
|
||||
func (m *MockRedis) isByteSlice(value interface{}) (string, bool) {
|
||||
// convert the []byte message into string
|
||||
if data, ok := value.([]byte); ok {
|
||||
return string(data), true
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (m *MockRedis) FetchCache(id string) (value ExpiringCache, ok bool) {
|
||||
value, ok = m.SetValues[id]
|
||||
return
|
||||
}
|
||||
|
||||
func (m *MockRedis) NewSet(id string, value interface{}, expires int) error {
|
||||
if m.Error != nil {
|
||||
return m.Error
|
||||
}
|
||||
|
||||
m.SetValues[id] = ExpiringCache{Value: value, Expires: expires}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockRedis) Ping() error {
|
||||
return m.Error
|
||||
}
|
||||
|
||||
func (m *MockRedis) GetObjectRaw(string) (map[string][]byte, error) {
|
||||
if m.Error != nil {
|
||||
return nil, m.Error
|
||||
}
|
||||
|
||||
return m.GetObjectRawResults, nil
|
||||
}
|
||||
|
||||
func (m *MockRedis) Reset() {
|
||||
m.ExecuteResults = nil
|
||||
m.GetResults = nil
|
||||
m.Error = nil
|
||||
m.PublishedMessages = map[string]interface{}{}
|
||||
m.SetValues = map[string]ExpiringCache{}
|
||||
m.ExecutedCommands = []interface{}{}
|
||||
m.Closed = false
|
||||
}
|
||||
|
||||
// We attempt to type convert our interfaces so we can copy them.
|
||||
// If we try to do the json.Marshal copy without doing this, then we get map[string]interface{}
|
||||
// which requires code changes to multiple different areas to get working
|
||||
func BeginDeepCopy(original interface{}) (copy interface{}, err error) {
|
||||
switch original.(type) {
|
||||
case common.Message:
|
||||
copy, err = deepCopyMessage(original.(common.Message))
|
||||
default:
|
||||
err = errors.New("no match")
|
||||
}
|
||||
if err != nil {
|
||||
return original, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// If we want to deep copy other data structs, just need to add their type in here
|
||||
func deepCopyMessage(original common.Message) (copy common.Message, err error) {
|
||||
copy = original
|
||||
switch original.Data.(type) {
|
||||
case ExampleDeepItem:
|
||||
data := original.Data.(ExampleDeepItem)
|
||||
copy.Data, err = copyObject(data)
|
||||
case common.UpdateManifest:
|
||||
data := original.Data.(common.UpdateManifest)
|
||||
copy.Data, err = copyObject(data)
|
||||
case common.CarUpdate:
|
||||
data := original.Data.(common.CarUpdate)
|
||||
copy.Data, err = copyObject(data)
|
||||
default:
|
||||
err = errors.New("no result")
|
||||
}
|
||||
if err != nil {
|
||||
return original, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// If this is used on an interface, it makes it a map[string]interface
|
||||
// so your object needs to be type casted first
|
||||
func copyObject[V any](original V) (copy V, err error) {
|
||||
b, err := json.Marshal(original)
|
||||
if err != nil {
|
||||
return original, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(b, ©)
|
||||
if err != nil {
|
||||
return original, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ExampleDeepItem struct {
|
||||
Title string
|
||||
NestedObject []*NestedDeepItem
|
||||
}
|
||||
|
||||
type NestedDeepItem struct {
|
||||
ID int
|
||||
Description *string
|
||||
}
|
||||
56
pkg/redis/tester/mock_client_pool.go
Normal file
56
pkg/redis/tester/mock_client_pool.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package tester
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"fiskerinc.com/modules/redis"
|
||||
)
|
||||
|
||||
func NewMockClientPool(args ...interface{}) redis.ClientPoolInterface {
|
||||
result := &MockClientPool{}
|
||||
|
||||
for i := range args {
|
||||
if pool, ok := args[i].(redis.Pool); ok {
|
||||
result.pool = pool
|
||||
} else if client, ok := args[i].(redis.Client); ok {
|
||||
result.client = client
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
type MockClientPool struct {
|
||||
once sync.Once
|
||||
oncePool sync.Once
|
||||
client redis.Client
|
||||
pool redis.Pool
|
||||
}
|
||||
|
||||
func (f *MockClientPool) getClient() redis.Client {
|
||||
f.once.Do(func() {
|
||||
if f.client == nil {
|
||||
f.client = NewRedisMock()
|
||||
}
|
||||
})
|
||||
|
||||
return f.client
|
||||
}
|
||||
|
||||
func (f *MockClientPool) GetPool() redis.Pool {
|
||||
f.oncePool.Do(func() {
|
||||
if f.pool == nil {
|
||||
f.pool = redis.GetMockPool()
|
||||
}
|
||||
})
|
||||
|
||||
return f.pool
|
||||
}
|
||||
|
||||
func (f *MockClientPool) SetPool(pool redis.Pool) {
|
||||
f.pool = pool
|
||||
}
|
||||
|
||||
func (f *MockClientPool) GetFromPool() redis.Client {
|
||||
return f.getClient()
|
||||
}
|
||||
58
pkg/redis/tester/mock_client_test.go
Normal file
58
pkg/redis/tester/mock_client_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package tester
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/common"
|
||||
"fiskerinc.com/modules/utils/elptr"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMockClientPublishMessage(t *testing.T) {
|
||||
testItem := ExampleDeepItem{}
|
||||
testItem.Title = "testItem"
|
||||
testItem.NestedObject = append(testItem.NestedObject,
|
||||
elptr.ElPtr(NestedDeepItem{ID: 0, Description: elptr.ElPtr("zero")}),
|
||||
elptr.ElPtr(NestedDeepItem{ID: 1, Description: elptr.ElPtr("one")}),
|
||||
)
|
||||
redisMock := NewRedisMock()
|
||||
redisMock.SafePublishMessage("0", common.Message{
|
||||
Handler: "first-handler",
|
||||
Data: testItem,
|
||||
})
|
||||
|
||||
// Now we do some changes to the testItem
|
||||
testItem.NestedObject[0].ID = 25
|
||||
testItem.NestedObject[0].Description = elptr.ElPtr("not zero")
|
||||
testItem.NestedObject = testItem.NestedObject[:1]
|
||||
|
||||
redisMock.PublishMessage("1", common.Message{
|
||||
Handler: "second-handler",
|
||||
Data: testItem,
|
||||
})
|
||||
|
||||
expectedItem1 := ExampleDeepItem{}
|
||||
expectedItem1.Title = "testItem"
|
||||
expectedItem1.NestedObject = append(expectedItem1.NestedObject,
|
||||
elptr.ElPtr(NestedDeepItem{ID: 0, Description: elptr.ElPtr("zero")}),
|
||||
elptr.ElPtr(NestedDeepItem{ID: 1, Description: elptr.ElPtr("one")}),
|
||||
)
|
||||
|
||||
expectedItem2 := ExampleDeepItem{}
|
||||
expectedItem2.Title = "testItem"
|
||||
expectedItem2.NestedObject = append(expectedItem2.NestedObject,
|
||||
elptr.ElPtr(NestedDeepItem{ID: 25, Description: elptr.ElPtr("not zero")}),
|
||||
)
|
||||
|
||||
expectedMessage := map[string]interface{}{
|
||||
"0": common.Message{
|
||||
Handler: "first-handler",
|
||||
Data: expectedItem1,
|
||||
},
|
||||
"1": common.Message{
|
||||
Handler: "second-handler",
|
||||
Data: expectedItem2,
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expectedMessage, redisMock.PublishedMessages)
|
||||
}
|
||||
13
pkg/redis/tester/mock_vehicles_cache.go
Normal file
13
pkg/redis/tester/mock_vehicles_cache.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package tester
|
||||
|
||||
import "fiskerinc.com/modules/cache"
|
||||
|
||||
type MockVehiclesCache struct{}
|
||||
|
||||
func (m *MockVehiclesCache) Set(key cache.VehiclesTTLParams, value *cache.VehiclesTTLResult) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockVehiclesCache) Get(key cache.VehiclesTTLParams) (*cache.VehiclesTTLResult, error) {
|
||||
return nil, nil
|
||||
}
|
||||
113
pkg/redis/tester/redis_test_case.go
Normal file
113
pkg/redis/tester/redis_test_case.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package tester
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"fiskerinc.com/modules/common"
|
||||
"fiskerinc.com/modules/testhelper"
|
||||
)
|
||||
|
||||
type ExpiringCacheResult struct {
|
||||
Value string
|
||||
Expires int
|
||||
RegexCompare *regexp.Regexp
|
||||
}
|
||||
|
||||
type RedisTestCase struct {
|
||||
Device common.Device
|
||||
DeviceKey string
|
||||
PayloadData string
|
||||
ExpectedError string
|
||||
ExpectedMessages map[string]string
|
||||
ExpectedCaches map[string]ExpiringCacheResult
|
||||
MockRedisError error
|
||||
MockRedisGet interface{}
|
||||
MockRedisGetCache string
|
||||
MockRedisRetrieve string
|
||||
MockRedisGetSet string
|
||||
MockRedisGetMulti []interface{}
|
||||
Setup func()
|
||||
}
|
||||
|
||||
func (tc *RedisTestCase) SetupRedis(mockRedis *MockRedis) {
|
||||
if mockRedis != nil {
|
||||
mockRedis.Error = tc.MockRedisError
|
||||
mockRedis.GetCacheResults = tc.MockRedisGetCache
|
||||
mockRedis.GetResults = tc.MockRedisGet
|
||||
mockRedis.GetSetResults = tc.MockRedisGetSet
|
||||
mockRedis.RetrieveResult = tc.MockRedisRetrieve
|
||||
mockRedis.GetMultiResults = tc.MockRedisGetMulti
|
||||
if tc.Setup != nil {
|
||||
tc.Setup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (tc *RedisTestCase) CheckHandlerError(t *testing.T, name string, err error) {
|
||||
if err != nil && err.Error() != tc.ExpectedError {
|
||||
t.Errorf(testhelper.TestErrorTemplate, name, tc.ExpectedError, err.Error())
|
||||
} else if err == nil && tc.ExpectedError != "" {
|
||||
t.Errorf(testhelper.TestErrorTemplate, name, tc.ExpectedError, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (tc *RedisTestCase) Validate(t *testing.T, name string, mock *MockRedis) {
|
||||
tc.checkRedisMessages(t, name, mock)
|
||||
tc.checkRedisCache(t, name, mock)
|
||||
}
|
||||
|
||||
func (tc *RedisTestCase) checkRedisMessages(t *testing.T, name string, mock *MockRedis) {
|
||||
for key, msg := range tc.ExpectedMessages {
|
||||
if compare, ok := mock.HasMessage(key, msg); !ok {
|
||||
t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("%s CheckRedisMessages %s", name, key), msg, compare)
|
||||
}
|
||||
}
|
||||
|
||||
for key := range mock.PublishedMessages {
|
||||
if _, ok := tc.ExpectedMessages[key]; !ok {
|
||||
t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("%s unexpected message %s", name, key), nil, fmt.Sprintf("%s = %s", key, mock.PublishedMessages[key]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (tc *RedisTestCase) checkRedisCache(t *testing.T, name string, mock *MockRedis) {
|
||||
for key, expected := range tc.ExpectedCaches {
|
||||
tc.checkCacheValues(t, name, mock, key, expected)
|
||||
}
|
||||
|
||||
for key := range mock.SetValues {
|
||||
if _, ok := tc.ExpectedCaches[key]; !ok {
|
||||
t.Errorf(testhelper.TestErrorTemplate, fmt.Sprintf("%s unexpected cache %s", name, key), nil, fmt.Sprintf("%s = %v", key, mock.SetValues[key]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (tc *RedisTestCase) getCacheValue(mock *MockRedis, cache ExpiringCache) (string, error) {
|
||||
return cache.StringValue()
|
||||
}
|
||||
|
||||
func (tc *RedisTestCase) checkCacheValues(t *testing.T, test string, mock *MockRedis, key string, expected ExpiringCacheResult) {
|
||||
name := fmt.Sprintf("%s checkCacheValues %s", test, key)
|
||||
if cached, ok := mock.FetchCache(key); ok {
|
||||
value, err := tc.getCacheValue(mock, cached)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if expected.RegexCompare != nil {
|
||||
if !expected.RegexCompare.Match([]byte(value)) {
|
||||
t.Errorf(testhelper.TestErrorTemplate, name, expected.RegexCompare.String(), value)
|
||||
}
|
||||
} else if value != expected.Value {
|
||||
t.Errorf(testhelper.TestErrorTemplate, name, expected.Value, value)
|
||||
}
|
||||
|
||||
if expected.Expires != cached.Expires {
|
||||
t.Errorf(testhelper.TestErrorTemplate, name, expected.Expires, cached.Expires)
|
||||
}
|
||||
} else {
|
||||
t.Errorf(testhelper.TestErrorTemplate, name, fmt.Sprintf("Has Cache %s", key), nil)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user