github.com/willyham/dosa@v2.3.1-0.20171024181418-1e446d37ee71+incompatible/connectors/redis/redis.go (about)

     1  // Copyright (c) 2017 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package redis
    22  
    23  import (
    24  	"context"
    25  	"fmt"
    26  	"strings"
    27  	"time"
    28  
    29  	"github.com/uber-go/dosa"
    30  	"github.com/uber-go/dosa/connectors/base"
    31  	"github.com/uber-go/dosa/metrics"
    32  )
    33  
    34  const keySeparator = ","
    35  
    36  // SimpleRedis is a minimal interface to Redis commands
    37  type SimpleRedis interface {
    38  	Get(key string) ([]byte, error)
    39  	SetEx(key string, value []byte, ttl time.Duration) error
    40  	Del(key string) error
    41  	Shutdown() error
    42  }
    43  
    44  // ErrNotImplemented is returned for interface methods that do not have an implementation
    45  type ErrNotImplemented struct{}
    46  
    47  // Error returns a constant string "Not implemented"
    48  func (*ErrNotImplemented) Error() string {
    49  	return "Not implemented"
    50  }
    51  
    52  // ErrInvalidEntity is returned for dosa entities that cannot be categorized as a key-value schema for redis
    53  type ErrInvalidEntity struct {
    54  	msg string
    55  }
    56  
    57  // Error returns why the schema does not conform to key-value format
    58  func (e *ErrInvalidEntity) Error() string {
    59  	return fmt.Sprintf("This entity schema and value not supported by redis. %v", e.msg)
    60  }
    61  
    62  // NewErrInvalidEntity returns an ErrInvalidEntity
    63  func NewErrInvalidEntity(msg string) *ErrInvalidEntity {
    64  	return &ErrInvalidEntity{msg: msg}
    65  }
    66  
    67  // Config holds the settings for a RedisConnector
    68  type Config struct {
    69  	// ServerSettings are the settings specific to redis server
    70  	ServerSettings ServerConfig
    71  	// TTL for how long values should live in the cache
    72  	TTL time.Duration
    73  }
    74  
    75  // ServerConfig holds the settings for redis
    76  type ServerConfig struct {
    77  	Host string
    78  	Port int
    79  	// MaxIdle is the maximum number of idle connections in the pool.
    80  	MaxIdle int
    81  	// IdleTimeout directs to close connections after remaining idle for this duration.
    82  	// If the value is zero, then idle connections are not closed. Applications should set
    83  	// the timeout to a value less than the server's timeout.
    84  	IdleTimeout time.Duration
    85  	// Maximum number of connections allocated by the pool at a given time.
    86  	// When zero, there is no limit on the number of connections in the pool.
    87  	MaxActive int
    88  
    89  	ConnectTimeout time.Duration
    90  	ReadTimeout    time.Duration
    91  	WriteTimeout   time.Duration
    92  }
    93  
    94  // NewConnector initializes a Redis Connector
    95  func NewConnector(config Config, scope metrics.Scope) dosa.Connector {
    96  	return &Connector{
    97  		client: NewRedigoClient(config.ServerSettings),
    98  		ttl:    config.TTL,
    99  		stats:  scope,
   100  	}
   101  }
   102  
   103  // Connector for redis database
   104  type Connector struct {
   105  	base.Connector
   106  	client SimpleRedis
   107  	ttl    time.Duration
   108  	stats  metrics.Scope
   109  }
   110  
   111  // CreateIfNotExists not implemented
   112  func (c *Connector) CreateIfNotExists(ctx context.Context, ei *dosa.EntityInfo, values map[string]dosa.FieldValue) error {
   113  	return new(ErrNotImplemented)
   114  }
   115  
   116  // MultiRead not implemented
   117  func (c *Connector) MultiRead(ctx context.Context, ei *dosa.EntityInfo, keys []map[string]dosa.FieldValue, minimumFields []string) (results []*dosa.FieldValuesOrError, err error) {
   118  	return nil, new(ErrNotImplemented)
   119  }
   120  
   121  // MultiUpsert not implemented
   122  func (c *Connector) MultiUpsert(ctx context.Context, ei *dosa.EntityInfo, multiValues []map[string]dosa.FieldValue) (result []error, err error) {
   123  	return nil, new(ErrNotImplemented)
   124  }
   125  
   126  // RemoveRange not implemented
   127  func (c *Connector) RemoveRange(ctx context.Context, ei *dosa.EntityInfo, columnConditions map[string][]*dosa.Condition) error {
   128  	return new(ErrNotImplemented)
   129  }
   130  
   131  // MultiRemove not implemented
   132  func (c *Connector) MultiRemove(ctx context.Context, ei *dosa.EntityInfo, multiKeys []map[string]dosa.FieldValue) (result []error, err error) {
   133  	return nil, new(ErrNotImplemented)
   134  }
   135  
   136  // Range not implemented.
   137  func (c *Connector) Range(ctx context.Context, ei *dosa.EntityInfo, columnConditions map[string][]*dosa.Condition, minimumFields []string, token string, limit int) ([]map[string]dosa.FieldValue, string, error) {
   138  	return nil, "", new(ErrNotImplemented)
   139  }
   140  
   141  // Scan not implemented.
   142  func (c *Connector) Scan(ctx context.Context, ei *dosa.EntityInfo, minimumFields []string, token string, limit int) (multiValues []map[string]dosa.FieldValue, nextToken string, err error) {
   143  	return nil, "", new(ErrNotImplemented)
   144  }
   145  
   146  // Shutdown not implemented
   147  func (c *Connector) Shutdown() error {
   148  	err := c.client.Shutdown()
   149  	c.logError("Shutdown", err)
   150  	return err
   151  }
   152  
   153  // Read reads an object based on primary key
   154  func (c *Connector) Read(ctx context.Context, ei *dosa.EntityInfo, keys map[string]dosa.FieldValue, fieldsToRead []string) (map[string]dosa.FieldValue, error) {
   155  	err := validateSchema(ei)
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  
   160  	keyName, valueName := nameOfKeyValue(ei)
   161  
   162  	cacheKey, err := buildKey(ei.Ref.Scope, ei.Ref.NamePrefix, ei.Def.Name, keys[keyName])
   163  	if err != nil {
   164  		return nil, err
   165  	}
   166  
   167  	cacheValue, err := c.client.Get(cacheKey)
   168  	c.logHitRate("Read", err)
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  
   173  	result := make(map[string]dosa.FieldValue)
   174  	// Copy original keys into response
   175  	for k, v := range keys {
   176  		result[k] = v
   177  	}
   178  	result[valueName] = cacheValue
   179  	return result, nil
   180  }
   181  
   182  // Upsert means update an existing object or create a new object
   183  func (c *Connector) Upsert(ctx context.Context, ei *dosa.EntityInfo, values map[string]dosa.FieldValue) error {
   184  	err := validateSchema(ei)
   185  	if err != nil {
   186  		return err
   187  	}
   188  
   189  	keyName, valueName := nameOfKeyValue(ei)
   190  
   191  	cacheValue, ok := values[valueName]
   192  	if !ok || cacheValue == nil {
   193  		return NewErrInvalidEntity("No value specified.")
   194  	}
   195  
   196  	cacheValueBytes := cacheValue.([]byte)
   197  	if len(cacheValueBytes) == 0 {
   198  		return NewErrInvalidEntity("No value specified.")
   199  	}
   200  
   201  	cacheKey, err := buildKey(ei.Ref.Scope, ei.Ref.NamePrefix, ei.Def.Name, values[keyName])
   202  	if err != nil {
   203  		return err
   204  	}
   205  
   206  	err = c.client.SetEx(cacheKey, cacheValueBytes, c.ttl)
   207  	c.logError("Upsert", err)
   208  	return err
   209  }
   210  
   211  // Remove deletes a key
   212  func (c *Connector) Remove(ctx context.Context, ei *dosa.EntityInfo, keys map[string]dosa.FieldValue) error {
   213  	err := validateSchema(ei)
   214  	if err != nil {
   215  		return err
   216  	}
   217  	keyName, _ := nameOfKeyValue(ei)
   218  	cacheKey, err := buildKey(ei.Ref.Scope, ei.Ref.NamePrefix, ei.Def.Name, keys[keyName])
   219  	if err != nil {
   220  		return err
   221  	}
   222  
   223  	err = c.client.Del(cacheKey)
   224  	c.logError("Remove", err)
   225  	return err
   226  }
   227  
   228  func (c *Connector) logHitRate(method string, err error) {
   229  	if err != nil {
   230  		if dosa.ErrorIsNotFound(err) {
   231  			c.incStat("miss", method)
   232  			return
   233  		}
   234  		c.logError(method, err)
   235  		return
   236  	}
   237  	c.incStat("hit", method)
   238  }
   239  
   240  func (c *Connector) logError(method string, err error) {
   241  	if err != nil {
   242  		c.incStat("error", method)
   243  	}
   244  }
   245  
   246  func (c *Connector) incStat(action, method string) {
   247  	if c.stats == nil {
   248  		return
   249  	}
   250  	c.stats.SubScope("cache").Tagged(map[string]string{"method": method}).Counter(action).Inc(1)
   251  }
   252  
   253  // return order is key, value
   254  func nameOfKeyValue(ei *dosa.EntityInfo) (string, string) {
   255  	keyName := ei.Def.Key.PartitionKeys[0]
   256  	cols := ei.Def.Columns
   257  	if cols[0].Name == keyName {
   258  		return keyName, cols[1].Name
   259  	}
   260  	return keyName, cols[0].Name
   261  }
   262  
   263  func validateSchema(ei *dosa.EntityInfo) error {
   264  	if len(ei.Def.Key.PartitionKeys) != 1 || len(ei.Def.Key.ClusteringKeys) != 0 {
   265  		return NewErrInvalidEntity("Should only have a single key.")
   266  	}
   267  	if len(ei.Def.Columns) != 2 {
   268  		return NewErrInvalidEntity("Should have one key, one value.")
   269  	}
   270  	if ei.Def.Columns[0].Type != dosa.Blob || ei.Def.Columns[1].Type != dosa.Blob {
   271  		return NewErrInvalidEntity("Types should be []byte.")
   272  	}
   273  	return nil
   274  }
   275  
   276  func buildKey(scope, namePrefix, name string, keyValue interface{}) (string, error) {
   277  	keyNamespace := strings.Join([]string{scope, namePrefix, name}, keySeparator)
   278  	keyString := keyValue.([]byte)
   279  	if len(keyString) == 0 {
   280  		return "", NewErrInvalidEntity("No key specified.")
   281  	}
   282  	return strings.Join([]string{keyNamespace, string(keyString)}, keySeparator), nil
   283  }