| // Copyright 2018 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package memcache provides a minimally compatible interface for |
| // google.golang.org/appengine/memcache |
| // and stores the data in Redis (e.g., via Cloud Memorystore). |
| package memcache |
| |
| import ( |
| "bytes" |
| "context" |
| "encoding/gob" |
| "encoding/json" |
| "errors" |
| "time" |
| |
| "github.com/gomodule/redigo/redis" |
| ) |
| |
| var ErrCacheMiss = errors.New("memcache: cache miss") |
| |
| func New(addr string) *Client { |
| const maxConns = 20 |
| |
| pool := redis.NewPool(func() (redis.Conn, error) { |
| return redis.Dial("tcp", addr) |
| }, maxConns) |
| |
| return &Client{ |
| pool: pool, |
| } |
| } |
| |
| type Client struct { |
| pool *redis.Pool |
| } |
| |
| type CodecClient struct { |
| client *Client |
| codec Codec |
| } |
| |
| type Item struct { |
| Key string |
| Value []byte |
| Object interface{} // Used with Codec. |
| Expiration time.Duration // Read-only. |
| } |
| |
| func (c *Client) WithCodec(codec Codec) *CodecClient { |
| return &CodecClient{ |
| c, codec, |
| } |
| } |
| |
| func (c *Client) Delete(ctx context.Context, key string) error { |
| conn, err := c.pool.GetContext(ctx) |
| if err != nil { |
| return err |
| } |
| defer conn.Close() |
| |
| _, err = conn.Do("DEL", key) |
| return err |
| } |
| |
| func (c *CodecClient) Delete(ctx context.Context, key string) error { |
| return c.client.Delete(ctx, key) |
| } |
| |
| func (c *Client) Set(ctx context.Context, item *Item) error { |
| if item.Value == nil { |
| return errors.New("nil item value") |
| } |
| return c.set(ctx, item.Key, item.Value, item.Expiration) |
| } |
| |
| func (c *CodecClient) Set(ctx context.Context, item *Item) error { |
| if item.Object == nil { |
| return errors.New("nil object value") |
| } |
| b, err := c.codec.Marshal(item.Object) |
| if err != nil { |
| return err |
| } |
| return c.client.set(ctx, item.Key, b, item.Expiration) |
| } |
| |
| func (c *Client) set(ctx context.Context, key string, value []byte, expiration time.Duration) error { |
| conn, err := c.pool.GetContext(ctx) |
| if err != nil { |
| return err |
| } |
| defer conn.Close() |
| |
| if expiration == 0 { |
| _, err := conn.Do("SET", key, value) |
| return err |
| } |
| |
| // NOTE(cbro): redis does not support expiry in units more granular than a second. |
| exp := int64(expiration.Seconds()) |
| if exp == 0 { |
| // Redis doesn't allow a zero expiration, delete the key instead. |
| _, err := conn.Do("DEL", key) |
| return err |
| } |
| |
| _, err = conn.Do("SETEX", key, exp, value) |
| return err |
| } |
| |
| // Get gets the item. |
| func (c *Client) Get(ctx context.Context, key string) ([]byte, error) { |
| conn, err := c.pool.GetContext(ctx) |
| if err != nil { |
| return nil, err |
| } |
| defer conn.Close() |
| |
| b, err := redis.Bytes(conn.Do("GET", key)) |
| if err == redis.ErrNil { |
| err = ErrCacheMiss |
| } |
| return b, err |
| } |
| |
| func (c *CodecClient) Get(ctx context.Context, key string, v interface{}) error { |
| b, err := c.client.Get(ctx, key) |
| if err != nil { |
| return err |
| } |
| return c.codec.Unmarshal(b, v) |
| } |
| |
| var ( |
| Gob = Codec{gobMarshal, gobUnmarshal} |
| JSON = Codec{json.Marshal, json.Unmarshal} |
| ) |
| |
| type Codec struct { |
| Marshal func(interface{}) ([]byte, error) |
| Unmarshal func([]byte, interface{}) error |
| } |
| |
| func gobMarshal(v interface{}) ([]byte, error) { |
| var buf bytes.Buffer |
| if err := gob.NewEncoder(&buf).Encode(v); err != nil { |
| return nil, err |
| } |
| return buf.Bytes(), nil |
| } |
| |
| func gobUnmarshal(data []byte, v interface{}) error { |
| return gob.NewDecoder(bytes.NewBuffer(data)).Decode(v) |
| } |