forked from mirror/ledisdb
copy garyburd redid lib to client
This commit is contained in:
parent
a71fbfbfd3
commit
50bd648e0c
|
@ -3,4 +3,3 @@
|
||||||
. ./dev.sh
|
. ./dev.sh
|
||||||
|
|
||||||
go get -u github.com/siddontang/go-log/log
|
go get -u github.com/siddontang/go-log/log
|
||||||
go get -u github.com/garyburd/redigo/redis
|
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
// Copyright 2014 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
watchState = 1 << iota
|
||||||
|
multiState
|
||||||
|
subscribeState
|
||||||
|
monitorState
|
||||||
|
)
|
||||||
|
|
||||||
|
type commandInfo struct {
|
||||||
|
set, clear int
|
||||||
|
}
|
||||||
|
|
||||||
|
var commandInfos = map[string]commandInfo{
|
||||||
|
"WATCH": commandInfo{set: watchState},
|
||||||
|
"UNWATCH": commandInfo{clear: watchState},
|
||||||
|
"MULTI": commandInfo{set: multiState},
|
||||||
|
"EXEC": commandInfo{clear: watchState | multiState},
|
||||||
|
"DISCARD": commandInfo{clear: watchState | multiState},
|
||||||
|
"PSUBSCRIBE": commandInfo{set: subscribeState},
|
||||||
|
"SUBSCRIBE": commandInfo{set: subscribeState},
|
||||||
|
"MONITOR": commandInfo{set: monitorState},
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupCommandInfo(commandName string) commandInfo {
|
||||||
|
return commandInfos[strings.ToUpper(commandName)]
|
||||||
|
}
|
|
@ -0,0 +1,418 @@
|
||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// conn is the low-level implementation of Conn
|
||||||
|
type conn struct {
|
||||||
|
|
||||||
|
// Shared
|
||||||
|
mu sync.Mutex
|
||||||
|
pending int
|
||||||
|
err error
|
||||||
|
conn net.Conn
|
||||||
|
|
||||||
|
// Read
|
||||||
|
readTimeout time.Duration
|
||||||
|
br *bufio.Reader
|
||||||
|
|
||||||
|
// Write
|
||||||
|
writeTimeout time.Duration
|
||||||
|
bw *bufio.Writer
|
||||||
|
|
||||||
|
// Scratch space for formatting argument length.
|
||||||
|
// '*' or '$', length, "\r\n"
|
||||||
|
lenScratch [32]byte
|
||||||
|
|
||||||
|
// Scratch space for formatting integers and floats.
|
||||||
|
numScratch [40]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dial connects to the Redis server at the given network and address.
|
||||||
|
func Dial(network, address string) (Conn, error) {
|
||||||
|
c, err := net.Dial(network, address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewConn(c, 0, 0), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DialTimeout acts like Dial but takes timeouts for establishing the
|
||||||
|
// connection to the server, writing a command and reading a reply.
|
||||||
|
func DialTimeout(network, address string, connectTimeout, readTimeout, writeTimeout time.Duration) (Conn, error) {
|
||||||
|
var c net.Conn
|
||||||
|
var err error
|
||||||
|
if connectTimeout > 0 {
|
||||||
|
c, err = net.DialTimeout(network, address, connectTimeout)
|
||||||
|
} else {
|
||||||
|
c, err = net.Dial(network, address)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewConn(c, readTimeout, writeTimeout), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConn returns a new Redigo connection for the given net connection.
|
||||||
|
func NewConn(netConn net.Conn, readTimeout, writeTimeout time.Duration) Conn {
|
||||||
|
return &conn{
|
||||||
|
conn: netConn,
|
||||||
|
bw: bufio.NewWriter(netConn),
|
||||||
|
br: bufio.NewReader(netConn),
|
||||||
|
readTimeout: readTimeout,
|
||||||
|
writeTimeout: writeTimeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) Close() error {
|
||||||
|
c.mu.Lock()
|
||||||
|
err := c.err
|
||||||
|
if c.err == nil {
|
||||||
|
c.err = errors.New("redigo: closed")
|
||||||
|
err = c.conn.Close()
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) fatal(err error) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
if c.err == nil {
|
||||||
|
c.err = err
|
||||||
|
// Close connection to force errors on subsequent calls and to unblock
|
||||||
|
// other reader or writer.
|
||||||
|
c.conn.Close()
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) Err() error {
|
||||||
|
c.mu.Lock()
|
||||||
|
err := c.err
|
||||||
|
c.mu.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) writeLen(prefix byte, n int) error {
|
||||||
|
c.lenScratch[len(c.lenScratch)-1] = '\n'
|
||||||
|
c.lenScratch[len(c.lenScratch)-2] = '\r'
|
||||||
|
i := len(c.lenScratch) - 3
|
||||||
|
for {
|
||||||
|
c.lenScratch[i] = byte('0' + n%10)
|
||||||
|
i -= 1
|
||||||
|
n = n / 10
|
||||||
|
if n == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.lenScratch[i] = prefix
|
||||||
|
_, err := c.bw.Write(c.lenScratch[i:])
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) writeString(s string) error {
|
||||||
|
c.writeLen('$', len(s))
|
||||||
|
c.bw.WriteString(s)
|
||||||
|
_, err := c.bw.WriteString("\r\n")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) writeBytes(p []byte) error {
|
||||||
|
c.writeLen('$', len(p))
|
||||||
|
c.bw.Write(p)
|
||||||
|
_, err := c.bw.WriteString("\r\n")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) writeInt64(n int64) error {
|
||||||
|
return c.writeBytes(strconv.AppendInt(c.numScratch[:0], n, 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) writeFloat64(n float64) error {
|
||||||
|
return c.writeBytes(strconv.AppendFloat(c.numScratch[:0], n, 'g', -1, 64))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) writeCommand(cmd string, args []interface{}) (err error) {
|
||||||
|
c.writeLen('*', 1+len(args))
|
||||||
|
err = c.writeString(cmd)
|
||||||
|
for _, arg := range args {
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
switch arg := arg.(type) {
|
||||||
|
case string:
|
||||||
|
err = c.writeString(arg)
|
||||||
|
case []byte:
|
||||||
|
err = c.writeBytes(arg)
|
||||||
|
case int:
|
||||||
|
err = c.writeInt64(int64(arg))
|
||||||
|
case int64:
|
||||||
|
err = c.writeInt64(arg)
|
||||||
|
case float64:
|
||||||
|
err = c.writeFloat64(arg)
|
||||||
|
case bool:
|
||||||
|
if arg {
|
||||||
|
err = c.writeString("1")
|
||||||
|
} else {
|
||||||
|
err = c.writeString("0")
|
||||||
|
}
|
||||||
|
case nil:
|
||||||
|
err = c.writeString("")
|
||||||
|
default:
|
||||||
|
var buf bytes.Buffer
|
||||||
|
fmt.Fprint(&buf, arg)
|
||||||
|
err = c.writeBytes(buf.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) readLine() ([]byte, error) {
|
||||||
|
p, err := c.br.ReadSlice('\n')
|
||||||
|
if err == bufio.ErrBufferFull {
|
||||||
|
return nil, errors.New("redigo: long response line")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
i := len(p) - 2
|
||||||
|
if i < 0 || p[i] != '\r' {
|
||||||
|
return nil, errors.New("redigo: bad response line terminator")
|
||||||
|
}
|
||||||
|
return p[:i], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseLen parses bulk string and array lengths.
|
||||||
|
func parseLen(p []byte) (int, error) {
|
||||||
|
if len(p) == 0 {
|
||||||
|
return -1, errors.New("redigo: malformed length")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p[0] == '-' && len(p) == 2 && p[1] == '1' {
|
||||||
|
// handle $-1 and $-1 null replies.
|
||||||
|
return -1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var n int
|
||||||
|
for _, b := range p {
|
||||||
|
n *= 10
|
||||||
|
if b < '0' || b > '9' {
|
||||||
|
return -1, errors.New("redigo: illegal bytes in length")
|
||||||
|
}
|
||||||
|
n += int(b - '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseInt parses an integer reply.
|
||||||
|
func parseInt(p []byte) (interface{}, error) {
|
||||||
|
if len(p) == 0 {
|
||||||
|
return 0, errors.New("redigo: malformed integer")
|
||||||
|
}
|
||||||
|
|
||||||
|
var negate bool
|
||||||
|
if p[0] == '-' {
|
||||||
|
negate = true
|
||||||
|
p = p[1:]
|
||||||
|
if len(p) == 0 {
|
||||||
|
return 0, errors.New("redigo: malformed integer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var n int64
|
||||||
|
for _, b := range p {
|
||||||
|
n *= 10
|
||||||
|
if b < '0' || b > '9' {
|
||||||
|
return 0, errors.New("redigo: illegal bytes in length")
|
||||||
|
}
|
||||||
|
n += int64(b - '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
if negate {
|
||||||
|
n = -n
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
okReply interface{} = "OK"
|
||||||
|
pongReply interface{} = "PONG"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *conn) readReply() (interface{}, error) {
|
||||||
|
line, err := c.readLine()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(line) == 0 {
|
||||||
|
return nil, errors.New("redigo: short response line")
|
||||||
|
}
|
||||||
|
switch line[0] {
|
||||||
|
case '+':
|
||||||
|
switch {
|
||||||
|
case len(line) == 3 && line[1] == 'O' && line[2] == 'K':
|
||||||
|
// Avoid allocation for frequent "+OK" response.
|
||||||
|
return okReply, nil
|
||||||
|
case len(line) == 5 && line[1] == 'P' && line[2] == 'O' && line[3] == 'N' && line[4] == 'G':
|
||||||
|
// Avoid allocation in PING command benchmarks :)
|
||||||
|
return pongReply, nil
|
||||||
|
default:
|
||||||
|
return string(line[1:]), nil
|
||||||
|
}
|
||||||
|
case '-':
|
||||||
|
return Error(string(line[1:])), nil
|
||||||
|
case ':':
|
||||||
|
return parseInt(line[1:])
|
||||||
|
case '$':
|
||||||
|
n, err := parseLen(line[1:])
|
||||||
|
if n < 0 || err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
p := make([]byte, n)
|
||||||
|
_, err = io.ReadFull(c.br, p)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if line, err := c.readLine(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if len(line) != 0 {
|
||||||
|
return nil, errors.New("redigo: bad bulk string format")
|
||||||
|
}
|
||||||
|
return p, nil
|
||||||
|
case '*':
|
||||||
|
n, err := parseLen(line[1:])
|
||||||
|
if n < 0 || err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r := make([]interface{}, n)
|
||||||
|
for i := range r {
|
||||||
|
r[i], err = c.readReply()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("redigo: unexpected response line")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) Send(cmd string, args ...interface{}) error {
|
||||||
|
c.mu.Lock()
|
||||||
|
c.pending += 1
|
||||||
|
c.mu.Unlock()
|
||||||
|
if c.writeTimeout != 0 {
|
||||||
|
c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
|
||||||
|
}
|
||||||
|
if err := c.writeCommand(cmd, args); err != nil {
|
||||||
|
return c.fatal(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) Flush() error {
|
||||||
|
if c.writeTimeout != 0 {
|
||||||
|
c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
|
||||||
|
}
|
||||||
|
if err := c.bw.Flush(); err != nil {
|
||||||
|
return c.fatal(err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) Receive() (reply interface{}, err error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
// There can be more receives than sends when using pub/sub. To allow
|
||||||
|
// normal use of the connection after unsubscribe from all channels, do not
|
||||||
|
// decrement pending to a negative value.
|
||||||
|
if c.pending > 0 {
|
||||||
|
c.pending -= 1
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
if c.readTimeout != 0 {
|
||||||
|
c.conn.SetReadDeadline(time.Now().Add(c.readTimeout))
|
||||||
|
}
|
||||||
|
if reply, err = c.readReply(); err != nil {
|
||||||
|
return nil, c.fatal(err)
|
||||||
|
}
|
||||||
|
if err, ok := reply.(Error); ok {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *conn) Do(cmd string, args ...interface{}) (interface{}, error) {
|
||||||
|
c.mu.Lock()
|
||||||
|
pending := c.pending
|
||||||
|
c.pending = 0
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
if cmd == "" && pending == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.writeTimeout != 0 {
|
||||||
|
c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout))
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd != "" {
|
||||||
|
c.writeCommand(cmd, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.bw.Flush(); err != nil {
|
||||||
|
return nil, c.fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.readTimeout != 0 {
|
||||||
|
c.conn.SetReadDeadline(time.Now().Add(c.readTimeout))
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd == "" {
|
||||||
|
reply := make([]interface{}, pending)
|
||||||
|
for i := range reply {
|
||||||
|
r, e := c.readReply()
|
||||||
|
if e != nil {
|
||||||
|
return nil, c.fatal(e)
|
||||||
|
}
|
||||||
|
reply[i] = r
|
||||||
|
}
|
||||||
|
return reply, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var reply interface{}
|
||||||
|
for i := 0; i <= pending; i++ {
|
||||||
|
var e error
|
||||||
|
if reply, e = c.readReply(); e != nil {
|
||||||
|
return nil, c.fatal(e)
|
||||||
|
}
|
||||||
|
if e, ok := reply.(Error); ok && err == nil {
|
||||||
|
err = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return reply, err
|
||||||
|
}
|
|
@ -0,0 +1,167 @@
|
||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
// Package redis is a client for the Redis database.
|
||||||
|
//
|
||||||
|
// The Redigo FAQ (https://github.com/garyburd/redigo/wiki/FAQ) contains more
|
||||||
|
// documentation about this package.
|
||||||
|
//
|
||||||
|
// Connections
|
||||||
|
//
|
||||||
|
// The Conn interface is the primary interface for working with Redis.
|
||||||
|
// Applications create connections by calling the Dial, DialWithTimeout or
|
||||||
|
// NewConn functions. In the future, functions will be added for creating
|
||||||
|
// sharded and other types of connections.
|
||||||
|
//
|
||||||
|
// The application must call the connection Close method when the application
|
||||||
|
// is done with the connection.
|
||||||
|
//
|
||||||
|
// Executing Commands
|
||||||
|
//
|
||||||
|
// The Conn interface has a generic method for executing Redis commands:
|
||||||
|
//
|
||||||
|
// Do(commandName string, args ...interface{}) (reply interface{}, err error)
|
||||||
|
//
|
||||||
|
// The Redis command reference (http://redis.io/commands) lists the available
|
||||||
|
// commands. An example of using the Redis APPEND command is:
|
||||||
|
//
|
||||||
|
// n, err := conn.Do("APPEND", "key", "value")
|
||||||
|
//
|
||||||
|
// The Do method converts command arguments to binary strings for transmission
|
||||||
|
// to the server as follows:
|
||||||
|
//
|
||||||
|
// Go Type Conversion
|
||||||
|
// []byte Sent as is
|
||||||
|
// string Sent as is
|
||||||
|
// int, int64 strconv.FormatInt(v)
|
||||||
|
// float64 strconv.FormatFloat(v, 'g', -1, 64)
|
||||||
|
// bool true -> "1", false -> "0"
|
||||||
|
// nil ""
|
||||||
|
// all other types fmt.Print(v)
|
||||||
|
//
|
||||||
|
// Redis command reply types are represented using the following Go types:
|
||||||
|
//
|
||||||
|
// Redis type Go type
|
||||||
|
// error redis.Error
|
||||||
|
// integer int64
|
||||||
|
// simple string string
|
||||||
|
// bulk string []byte or nil if value not present.
|
||||||
|
// array []interface{} or nil if value not present.
|
||||||
|
//
|
||||||
|
// Use type assertions or the reply helper functions to convert from
|
||||||
|
// interface{} to the specific Go type for the command result.
|
||||||
|
//
|
||||||
|
// Pipelining
|
||||||
|
//
|
||||||
|
// Connections support pipelining using the Send, Flush and Receive methods.
|
||||||
|
//
|
||||||
|
// Send(commandName string, args ...interface{}) error
|
||||||
|
// Flush() error
|
||||||
|
// Receive() (reply interface{}, err error)
|
||||||
|
//
|
||||||
|
// Send writes the command to the connection's output buffer. Flush flushes the
|
||||||
|
// connection's output buffer to the server. Receive reads a single reply from
|
||||||
|
// the server. The following example shows a simple pipeline.
|
||||||
|
//
|
||||||
|
// c.Send("SET", "foo", "bar")
|
||||||
|
// c.Send("GET", "foo")
|
||||||
|
// c.Flush()
|
||||||
|
// c.Receive() // reply from SET
|
||||||
|
// v, err = c.Receive() // reply from GET
|
||||||
|
//
|
||||||
|
// The Do method combines the functionality of the Send, Flush and Receive
|
||||||
|
// methods. The Do method starts by writing the command and flushing the output
|
||||||
|
// buffer. Next, the Do method receives all pending replies including the reply
|
||||||
|
// for the command just sent by Do. If any of the received replies is an error,
|
||||||
|
// then Do returns the error. If there are no errors, then Do returns the last
|
||||||
|
// reply. If the command argument to the Do method is "", then the Do method
|
||||||
|
// will flush the output buffer and receive pending replies without sending a
|
||||||
|
// command.
|
||||||
|
//
|
||||||
|
// Use the Send and Do methods to implement pipelined transactions.
|
||||||
|
//
|
||||||
|
// c.Send("MULTI")
|
||||||
|
// c.Send("INCR", "foo")
|
||||||
|
// c.Send("INCR", "bar")
|
||||||
|
// r, err := c.Do("EXEC")
|
||||||
|
// fmt.Println(r) // prints [1, 1]
|
||||||
|
//
|
||||||
|
// Concurrency
|
||||||
|
//
|
||||||
|
// Connections support a single concurrent caller to the write methods (Send,
|
||||||
|
// Flush) and a single concurrent caller to the read method (Receive). Because
|
||||||
|
// Do method combines the functionality of Send, Flush and Receive, the Do
|
||||||
|
// method cannot be called concurrently with the other methods.
|
||||||
|
//
|
||||||
|
// For full concurrent access to Redis, use the thread-safe Pool to get and
|
||||||
|
// release connections from within a goroutine.
|
||||||
|
//
|
||||||
|
// Publish and Subscribe
|
||||||
|
//
|
||||||
|
// Use the Send, Flush and Receive methods to implement Pub/Sub subscribers.
|
||||||
|
//
|
||||||
|
// c.Send("SUBSCRIBE", "example")
|
||||||
|
// c.Flush()
|
||||||
|
// for {
|
||||||
|
// reply, err := c.Receive()
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// // process pushed message
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// The PubSubConn type wraps a Conn with convenience methods for implementing
|
||||||
|
// subscribers. The Subscribe, PSubscribe, Unsubscribe and PUnsubscribe methods
|
||||||
|
// send and flush a subscription management command. The receive method
|
||||||
|
// converts a pushed message to convenient types for use in a type switch.
|
||||||
|
//
|
||||||
|
// psc := PubSubConn{c}
|
||||||
|
// psc.Subscribe("example")
|
||||||
|
// for {
|
||||||
|
// switch v := psc.Receive().(type) {
|
||||||
|
// case redis.Message:
|
||||||
|
// fmt.Printf("%s: message: %s\n", v.Channel, v.Data)
|
||||||
|
// case redis.Subscription:
|
||||||
|
// fmt.Printf("%s: %s %d\n", v.Channel, v.Kind, v.Count)
|
||||||
|
// case error:
|
||||||
|
// return v
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Reply Helpers
|
||||||
|
//
|
||||||
|
// The Bool, Int, Bytes, String, Strings and Values functions convert a reply
|
||||||
|
// to a value of a specific type. To allow convenient wrapping of calls to the
|
||||||
|
// connection Do and Receive methods, the functions take a second argument of
|
||||||
|
// type error. If the error is non-nil, then the helper function returns the
|
||||||
|
// error. If the error is nil, the function converts the reply to the specified
|
||||||
|
// type:
|
||||||
|
//
|
||||||
|
// exists, err := redis.Bool(c.Do("EXISTS", "foo"))
|
||||||
|
// if err != nil {
|
||||||
|
// // handle error return from c.Do or type conversion error.
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// The Scan function converts elements of a array reply to Go types:
|
||||||
|
//
|
||||||
|
// var value1 int
|
||||||
|
// var value2 string
|
||||||
|
// reply, err := redis.Values(c.Do("MGET", "key1", "key2"))
|
||||||
|
// if err != nil {
|
||||||
|
// // handle error
|
||||||
|
// }
|
||||||
|
// if _, err := redis.Scan(reply, &value1, &value2); err != nil {
|
||||||
|
// // handle error
|
||||||
|
// }
|
||||||
|
package redis
|
|
@ -0,0 +1,117 @@
|
||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewLoggingConn returns a logging wrapper around a connection.
|
||||||
|
func NewLoggingConn(conn Conn, logger *log.Logger, prefix string) Conn {
|
||||||
|
if prefix != "" {
|
||||||
|
prefix = prefix + "."
|
||||||
|
}
|
||||||
|
return &loggingConn{conn, logger, prefix}
|
||||||
|
}
|
||||||
|
|
||||||
|
type loggingConn struct {
|
||||||
|
Conn
|
||||||
|
logger *log.Logger
|
||||||
|
prefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *loggingConn) Close() error {
|
||||||
|
err := c.Conn.Close()
|
||||||
|
var buf bytes.Buffer
|
||||||
|
fmt.Fprintf(&buf, "%sClose() -> (%v)", c.prefix, err)
|
||||||
|
c.logger.Output(2, buf.String())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *loggingConn) printValue(buf *bytes.Buffer, v interface{}) {
|
||||||
|
const chop = 32
|
||||||
|
switch v := v.(type) {
|
||||||
|
case []byte:
|
||||||
|
if len(v) > chop {
|
||||||
|
fmt.Fprintf(buf, "%q...", v[:chop])
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(buf, "%q", v)
|
||||||
|
}
|
||||||
|
case string:
|
||||||
|
if len(v) > chop {
|
||||||
|
fmt.Fprintf(buf, "%q...", v[:chop])
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(buf, "%q", v)
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
if len(v) == 0 {
|
||||||
|
buf.WriteString("[]")
|
||||||
|
} else {
|
||||||
|
sep := "["
|
||||||
|
fin := "]"
|
||||||
|
if len(v) > chop {
|
||||||
|
v = v[:chop]
|
||||||
|
fin = "...]"
|
||||||
|
}
|
||||||
|
for _, vv := range v {
|
||||||
|
buf.WriteString(sep)
|
||||||
|
c.printValue(buf, vv)
|
||||||
|
sep = ", "
|
||||||
|
}
|
||||||
|
buf.WriteString(fin)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
fmt.Fprint(buf, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *loggingConn) print(method, commandName string, args []interface{}, reply interface{}, err error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
fmt.Fprintf(&buf, "%s%s(", c.prefix, method)
|
||||||
|
if method != "Receive" {
|
||||||
|
buf.WriteString(commandName)
|
||||||
|
for _, arg := range args {
|
||||||
|
buf.WriteString(", ")
|
||||||
|
c.printValue(&buf, arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.WriteString(") -> (")
|
||||||
|
if method != "Send" {
|
||||||
|
c.printValue(&buf, reply)
|
||||||
|
buf.WriteString(", ")
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&buf, "%v)", err)
|
||||||
|
c.logger.Output(3, buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *loggingConn) Do(commandName string, args ...interface{}) (interface{}, error) {
|
||||||
|
reply, err := c.Conn.Do(commandName, args...)
|
||||||
|
c.print("Do", commandName, args, reply, err)
|
||||||
|
return reply, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *loggingConn) Send(commandName string, args ...interface{}) error {
|
||||||
|
err := c.Conn.Send(commandName, args...)
|
||||||
|
c.print("Send", commandName, args, nil, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *loggingConn) Receive() (interface{}, error) {
|
||||||
|
reply, err := c.Conn.Receive()
|
||||||
|
c.print("Receive", "", nil, reply, err)
|
||||||
|
return reply, err
|
||||||
|
}
|
|
@ -0,0 +1,358 @@
|
||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"container/list"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha1"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var nowFunc = time.Now // for testing
|
||||||
|
|
||||||
|
// ErrPoolExhausted is returned from a pool connection method (Do, Send,
|
||||||
|
// Receive, Flush, Err) when the maximum number of database connections in the
|
||||||
|
// pool has been reached.
|
||||||
|
var ErrPoolExhausted = errors.New("redigo: connection pool exhausted")
|
||||||
|
|
||||||
|
var errPoolClosed = errors.New("redigo: connection pool closed")
|
||||||
|
|
||||||
|
// Pool maintains a pool of connections. The application calls the Get method
|
||||||
|
// to get a connection from the pool and the connection's Close method to
|
||||||
|
// return the connection's resources to the pool.
|
||||||
|
//
|
||||||
|
// The following example shows how to use a pool in a web application. The
|
||||||
|
// application creates a pool at application startup and makes it available to
|
||||||
|
// request handlers using a global variable.
|
||||||
|
//
|
||||||
|
// func newPool(server, password string) *redis.Pool {
|
||||||
|
// return &redis.Pool{
|
||||||
|
// MaxIdle: 3,
|
||||||
|
// IdleTimeout: 240 * time.Second,
|
||||||
|
// Dial: func () (redis.Conn, error) {
|
||||||
|
// c, err := redis.Dial("tcp", server)
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
// if _, err := c.Do("AUTH", password); err != nil {
|
||||||
|
// c.Close()
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
// return c, err
|
||||||
|
// },
|
||||||
|
// TestOnBorrow: func(c redis.Conn, t time.Time) error {
|
||||||
|
// _, err := c.Do("PING")
|
||||||
|
// return err
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// var (
|
||||||
|
// pool *redis.Pool
|
||||||
|
// redisServer = flag.String("redisServer", ":6379", "")
|
||||||
|
// redisPassword = flag.String("redisPassword", "", "")
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// func main() {
|
||||||
|
// flag.Parse()
|
||||||
|
// pool = newPool(*redisServer, *redisPassword)
|
||||||
|
// ...
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// A request handler gets a connection from the pool and closes the connection
|
||||||
|
// when the handler is done:
|
||||||
|
//
|
||||||
|
// func serveHome(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// conn := pool.Get()
|
||||||
|
// defer conn.Close()
|
||||||
|
// ....
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
type Pool struct {
|
||||||
|
|
||||||
|
// Dial is an application supplied function for creating new connections.
|
||||||
|
Dial func() (Conn, error)
|
||||||
|
|
||||||
|
// TestOnBorrow is an optional application supplied function for checking
|
||||||
|
// the health of an idle connection before the connection is used again by
|
||||||
|
// the application. Argument t is the time that the connection was returned
|
||||||
|
// to the pool. If the function returns an error, then the connection is
|
||||||
|
// closed.
|
||||||
|
TestOnBorrow func(c Conn, t time.Time) error
|
||||||
|
|
||||||
|
// Maximum number of idle connections in the pool.
|
||||||
|
MaxIdle int
|
||||||
|
|
||||||
|
// Maximum number of connections allocated by the pool at a given time.
|
||||||
|
// When zero, there is no limit on the number of connections in the pool.
|
||||||
|
MaxActive int
|
||||||
|
|
||||||
|
// Close connections after remaining idle for this duration. If the value
|
||||||
|
// is zero, then idle connections are not closed. Applications should set
|
||||||
|
// the timeout to a value less than the server's timeout.
|
||||||
|
IdleTimeout time.Duration
|
||||||
|
|
||||||
|
// mu protects fields defined below.
|
||||||
|
mu sync.Mutex
|
||||||
|
closed bool
|
||||||
|
active int
|
||||||
|
|
||||||
|
// Stack of idleConn with most recently used at the front.
|
||||||
|
idle list.List
|
||||||
|
}
|
||||||
|
|
||||||
|
type idleConn struct {
|
||||||
|
c Conn
|
||||||
|
t time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPool is a convenience function for initializing a pool.
|
||||||
|
func NewPool(newFn func() (Conn, error), maxIdle int) *Pool {
|
||||||
|
return &Pool{Dial: newFn, MaxIdle: maxIdle}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get gets a connection. The application must close the returned connection.
|
||||||
|
// The connection acquires an underlying connection on the first call to the
|
||||||
|
// connection Do, Send, Receive, Flush or Err methods. An application can force
|
||||||
|
// the connection to acquire an underlying connection without executing a Redis
|
||||||
|
// command by calling the Err method.
|
||||||
|
func (p *Pool) Get() Conn {
|
||||||
|
return &pooledConnection{p: p}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActiveCount returns the number of active connections in the pool.
|
||||||
|
func (p *Pool) ActiveCount() int {
|
||||||
|
p.mu.Lock()
|
||||||
|
active := p.active
|
||||||
|
p.mu.Unlock()
|
||||||
|
return active
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close releases the resources used by the pool.
|
||||||
|
func (p *Pool) Close() error {
|
||||||
|
p.mu.Lock()
|
||||||
|
idle := p.idle
|
||||||
|
p.idle.Init()
|
||||||
|
p.closed = true
|
||||||
|
p.active -= idle.Len()
|
||||||
|
p.mu.Unlock()
|
||||||
|
for e := idle.Front(); e != nil; e = e.Next() {
|
||||||
|
e.Value.(idleConn).c.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// get prunes stale connections and returns a connection from the idle list or
|
||||||
|
// creates a new connection.
|
||||||
|
func (p *Pool) get() (Conn, error) {
|
||||||
|
p.mu.Lock()
|
||||||
|
|
||||||
|
if p.closed {
|
||||||
|
p.mu.Unlock()
|
||||||
|
return nil, errors.New("redigo: get on closed pool")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prune stale connections.
|
||||||
|
|
||||||
|
if timeout := p.IdleTimeout; timeout > 0 {
|
||||||
|
for i, n := 0, p.idle.Len(); i < n; i++ {
|
||||||
|
e := p.idle.Back()
|
||||||
|
if e == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ic := e.Value.(idleConn)
|
||||||
|
if ic.t.Add(timeout).After(nowFunc()) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
p.idle.Remove(e)
|
||||||
|
p.active -= 1
|
||||||
|
p.mu.Unlock()
|
||||||
|
ic.c.Close()
|
||||||
|
p.mu.Lock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get idle connection.
|
||||||
|
|
||||||
|
for i, n := 0, p.idle.Len(); i < n; i++ {
|
||||||
|
e := p.idle.Front()
|
||||||
|
if e == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
ic := e.Value.(idleConn)
|
||||||
|
p.idle.Remove(e)
|
||||||
|
test := p.TestOnBorrow
|
||||||
|
p.mu.Unlock()
|
||||||
|
if test == nil || test(ic.c, ic.t) == nil {
|
||||||
|
return ic.c, nil
|
||||||
|
}
|
||||||
|
ic.c.Close()
|
||||||
|
p.mu.Lock()
|
||||||
|
p.active -= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.MaxActive > 0 && p.active >= p.MaxActive {
|
||||||
|
p.mu.Unlock()
|
||||||
|
return nil, ErrPoolExhausted
|
||||||
|
}
|
||||||
|
|
||||||
|
// No idle connection, create new.
|
||||||
|
|
||||||
|
dial := p.Dial
|
||||||
|
p.active += 1
|
||||||
|
p.mu.Unlock()
|
||||||
|
c, err := dial()
|
||||||
|
if err != nil {
|
||||||
|
p.mu.Lock()
|
||||||
|
p.active -= 1
|
||||||
|
p.mu.Unlock()
|
||||||
|
c = nil
|
||||||
|
}
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pool) put(c Conn, forceClose bool) error {
|
||||||
|
if c.Err() == nil && !forceClose {
|
||||||
|
p.mu.Lock()
|
||||||
|
if !p.closed {
|
||||||
|
p.idle.PushFront(idleConn{t: nowFunc(), c: c})
|
||||||
|
if p.idle.Len() > p.MaxIdle {
|
||||||
|
c = p.idle.Remove(p.idle.Back()).(idleConn).c
|
||||||
|
} else {
|
||||||
|
c = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.mu.Unlock()
|
||||||
|
}
|
||||||
|
if c != nil {
|
||||||
|
p.mu.Lock()
|
||||||
|
p.active -= 1
|
||||||
|
p.mu.Unlock()
|
||||||
|
return c.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type pooledConnection struct {
|
||||||
|
c Conn
|
||||||
|
err error
|
||||||
|
p *Pool
|
||||||
|
state int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *pooledConnection) get() error {
|
||||||
|
if c.err == nil && c.c == nil {
|
||||||
|
c.c, c.err = c.p.get()
|
||||||
|
}
|
||||||
|
return c.err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
sentinel []byte
|
||||||
|
sentinelOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func initSentinel() {
|
||||||
|
p := make([]byte, 64)
|
||||||
|
if _, err := rand.Read(p); err == nil {
|
||||||
|
sentinel = p
|
||||||
|
} else {
|
||||||
|
h := sha1.New()
|
||||||
|
io.WriteString(h, "Oops, rand failed. Use time instead.")
|
||||||
|
io.WriteString(h, strconv.FormatInt(time.Now().UnixNano(), 10))
|
||||||
|
sentinel = h.Sum(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *pooledConnection) Close() (err error) {
|
||||||
|
if c.c != nil {
|
||||||
|
if c.state&multiState != 0 {
|
||||||
|
c.c.Send("DISCARD")
|
||||||
|
c.state &^= (multiState | watchState)
|
||||||
|
} else if c.state&watchState != 0 {
|
||||||
|
c.c.Send("UNWATCH")
|
||||||
|
c.state &^= watchState
|
||||||
|
}
|
||||||
|
if c.state&subscribeState != 0 {
|
||||||
|
c.c.Send("UNSUBSCRIBE")
|
||||||
|
c.c.Send("PUNSUBSCRIBE")
|
||||||
|
// To detect the end of the message stream, ask the server to echo
|
||||||
|
// a sentinel value and read until we see that value.
|
||||||
|
sentinelOnce.Do(initSentinel)
|
||||||
|
c.c.Send("ECHO", sentinel)
|
||||||
|
c.c.Flush()
|
||||||
|
for {
|
||||||
|
p, err := c.c.Receive()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if p, ok := p.([]byte); ok && bytes.Equal(p, sentinel) {
|
||||||
|
c.state &^= subscribeState
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.c.Do("")
|
||||||
|
c.p.put(c.c, c.state != 0)
|
||||||
|
c.c = nil
|
||||||
|
c.err = errPoolClosed
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *pooledConnection) Err() error {
|
||||||
|
if err := c.get(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.c.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *pooledConnection) Do(commandName string, args ...interface{}) (reply interface{}, err error) {
|
||||||
|
if err := c.get(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ci := lookupCommandInfo(commandName)
|
||||||
|
c.state = (c.state | ci.set) &^ ci.clear
|
||||||
|
return c.c.Do(commandName, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *pooledConnection) Send(commandName string, args ...interface{}) error {
|
||||||
|
if err := c.get(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ci := lookupCommandInfo(commandName)
|
||||||
|
c.state = (c.state | ci.set) &^ ci.clear
|
||||||
|
return c.c.Send(commandName, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *pooledConnection) Flush() error {
|
||||||
|
if err := c.get(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.c.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *pooledConnection) Receive() (reply interface{}, err error) {
|
||||||
|
if err := c.get(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return c.c.Receive()
|
||||||
|
}
|
|
@ -0,0 +1,129 @@
|
||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Subscription represents a subscribe or unsubscribe notification.
|
||||||
|
type Subscription struct {
|
||||||
|
|
||||||
|
// Kind is "subscribe", "unsubscribe", "psubscribe" or "punsubscribe"
|
||||||
|
Kind string
|
||||||
|
|
||||||
|
// The channel that was changed.
|
||||||
|
Channel string
|
||||||
|
|
||||||
|
// The current number of subscriptions for connection.
|
||||||
|
Count int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message represents a message notification.
|
||||||
|
type Message struct {
|
||||||
|
|
||||||
|
// The originating channel.
|
||||||
|
Channel string
|
||||||
|
|
||||||
|
// The message data.
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// PMessage represents a pmessage notification.
|
||||||
|
type PMessage struct {
|
||||||
|
|
||||||
|
// The matched pattern.
|
||||||
|
Pattern string
|
||||||
|
|
||||||
|
// The originating channel.
|
||||||
|
Channel string
|
||||||
|
|
||||||
|
// The message data.
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// PubSubConn wraps a Conn with convenience methods for subscribers.
|
||||||
|
type PubSubConn struct {
|
||||||
|
Conn Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the connection.
|
||||||
|
func (c PubSubConn) Close() error {
|
||||||
|
return c.Conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe subscribes the connection to the specified channels.
|
||||||
|
func (c PubSubConn) Subscribe(channel ...interface{}) error {
|
||||||
|
c.Conn.Send("SUBSCRIBE", channel...)
|
||||||
|
return c.Conn.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PSubscribe subscribes the connection to the given patterns.
|
||||||
|
func (c PubSubConn) PSubscribe(channel ...interface{}) error {
|
||||||
|
c.Conn.Send("PSUBSCRIBE", channel...)
|
||||||
|
return c.Conn.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe unsubscribes the connection from the given channels, or from all
|
||||||
|
// of them if none is given.
|
||||||
|
func (c PubSubConn) Unsubscribe(channel ...interface{}) error {
|
||||||
|
c.Conn.Send("UNSUBSCRIBE", channel...)
|
||||||
|
return c.Conn.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUnsubscribe unsubscribes the connection from the given patterns, or from all
|
||||||
|
// of them if none is given.
|
||||||
|
func (c PubSubConn) PUnsubscribe(channel ...interface{}) error {
|
||||||
|
c.Conn.Send("PUNSUBSCRIBE", channel...)
|
||||||
|
return c.Conn.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receive returns a pushed message as a Subscription, Message, PMessage or
|
||||||
|
// error. The return value is intended to be used directly in a type switch as
|
||||||
|
// illustrated in the PubSubConn example.
|
||||||
|
func (c PubSubConn) Receive() interface{} {
|
||||||
|
reply, err := Values(c.Conn.Receive())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var kind string
|
||||||
|
reply, err = Scan(reply, &kind)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch kind {
|
||||||
|
case "message":
|
||||||
|
var m Message
|
||||||
|
if _, err := Scan(reply, &m.Channel, &m.Data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
case "pmessage":
|
||||||
|
var pm PMessage
|
||||||
|
if _, err := Scan(reply, &pm.Pattern, &pm.Channel, &pm.Data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return pm
|
||||||
|
case "subscribe", "psubscribe", "unsubscribe", "punsubscribe":
|
||||||
|
s := Subscription{Kind: kind}
|
||||||
|
if _, err := Scan(reply, &s.Channel, &s.Count); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return errors.New("redigo: unknown pubsub notification")
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
// Error represents an error returned in a command reply.
|
||||||
|
type Error string
|
||||||
|
|
||||||
|
func (err Error) Error() string { return string(err) }
|
||||||
|
|
||||||
|
// Conn represents a connection to a Redis server.
|
||||||
|
type Conn interface {
|
||||||
|
// Close closes the connection.
|
||||||
|
Close() error
|
||||||
|
|
||||||
|
// Err returns a non-nil value if the connection is broken. The returned
|
||||||
|
// value is either the first non-nil value returned from the underlying
|
||||||
|
// network connection or a protocol parsing error. Applications should
|
||||||
|
// close broken connections.
|
||||||
|
Err() error
|
||||||
|
|
||||||
|
// Do sends a command to the server and returns the received reply.
|
||||||
|
Do(commandName string, args ...interface{}) (reply interface{}, err error)
|
||||||
|
|
||||||
|
// Send writes the command to the client's output buffer.
|
||||||
|
Send(commandName string, args ...interface{}) error
|
||||||
|
|
||||||
|
// Flush flushes the output buffer to the Redis server.
|
||||||
|
Flush() error
|
||||||
|
|
||||||
|
// Receive receives a single reply from the Redis server
|
||||||
|
Receive() (reply interface{}, err error)
|
||||||
|
}
|
|
@ -0,0 +1,271 @@
|
||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrNil indicates that a reply value is nil.
|
||||||
|
var ErrNil = errors.New("redigo: nil returned")
|
||||||
|
|
||||||
|
// Int is a helper that converts a command reply to an integer. If err is not
|
||||||
|
// equal to nil, then Int returns 0, err. Otherwise, Int converts the
|
||||||
|
// reply to an int as follows:
|
||||||
|
//
|
||||||
|
// Reply type Result
|
||||||
|
// integer int(reply), nil
|
||||||
|
// bulk string parsed reply, nil
|
||||||
|
// nil 0, ErrNil
|
||||||
|
// other 0, error
|
||||||
|
func Int(reply interface{}, err error) (int, error) {
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case int64:
|
||||||
|
x := int(reply)
|
||||||
|
if int64(x) != reply {
|
||||||
|
return 0, strconv.ErrRange
|
||||||
|
}
|
||||||
|
return x, nil
|
||||||
|
case []byte:
|
||||||
|
n, err := strconv.ParseInt(string(reply), 10, 0)
|
||||||
|
return int(n), err
|
||||||
|
case nil:
|
||||||
|
return 0, ErrNil
|
||||||
|
case Error:
|
||||||
|
return 0, reply
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("redigo: unexpected type for Int, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int64 is a helper that converts a command reply to 64 bit integer. If err is
|
||||||
|
// not equal to nil, then Int returns 0, err. Otherwise, Int64 converts the
|
||||||
|
// reply to an int64 as follows:
|
||||||
|
//
|
||||||
|
// Reply type Result
|
||||||
|
// integer reply, nil
|
||||||
|
// bulk string parsed reply, nil
|
||||||
|
// nil 0, ErrNil
|
||||||
|
// other 0, error
|
||||||
|
func Int64(reply interface{}, err error) (int64, error) {
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case int64:
|
||||||
|
return reply, nil
|
||||||
|
case []byte:
|
||||||
|
n, err := strconv.ParseInt(string(reply), 10, 64)
|
||||||
|
return n, err
|
||||||
|
case nil:
|
||||||
|
return 0, ErrNil
|
||||||
|
case Error:
|
||||||
|
return 0, reply
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("redigo: unexpected type for Int64, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
var errNegativeInt = errors.New("redigo: unexpected value for Uint64")
|
||||||
|
|
||||||
|
// Uint64 is a helper that converts a command reply to 64 bit integer. If err is
|
||||||
|
// not equal to nil, then Int returns 0, err. Otherwise, Int64 converts the
|
||||||
|
// reply to an int64 as follows:
|
||||||
|
//
|
||||||
|
// Reply type Result
|
||||||
|
// integer reply, nil
|
||||||
|
// bulk string parsed reply, nil
|
||||||
|
// nil 0, ErrNil
|
||||||
|
// other 0, error
|
||||||
|
func Uint64(reply interface{}, err error) (uint64, error) {
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case int64:
|
||||||
|
if reply < 0 {
|
||||||
|
return 0, errNegativeInt
|
||||||
|
}
|
||||||
|
return uint64(reply), nil
|
||||||
|
case []byte:
|
||||||
|
n, err := strconv.ParseUint(string(reply), 10, 64)
|
||||||
|
return n, err
|
||||||
|
case nil:
|
||||||
|
return 0, ErrNil
|
||||||
|
case Error:
|
||||||
|
return 0, reply
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("redigo: unexpected type for Uint64, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float64 is a helper that converts a command reply to 64 bit float. If err is
|
||||||
|
// not equal to nil, then Float64 returns 0, err. Otherwise, Float64 converts
|
||||||
|
// the reply to an int as follows:
|
||||||
|
//
|
||||||
|
// Reply type Result
|
||||||
|
// bulk string parsed reply, nil
|
||||||
|
// nil 0, ErrNil
|
||||||
|
// other 0, error
|
||||||
|
func Float64(reply interface{}, err error) (float64, error) {
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case []byte:
|
||||||
|
n, err := strconv.ParseFloat(string(reply), 64)
|
||||||
|
return n, err
|
||||||
|
case nil:
|
||||||
|
return 0, ErrNil
|
||||||
|
case Error:
|
||||||
|
return 0, reply
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("redigo: unexpected type for Float64, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String is a helper that converts a command reply to a string. If err is not
|
||||||
|
// equal to nil, then String returns "", err. Otherwise String converts the
|
||||||
|
// reply to a string as follows:
|
||||||
|
//
|
||||||
|
// Reply type Result
|
||||||
|
// bulk string string(reply), nil
|
||||||
|
// simple string reply, nil
|
||||||
|
// nil "", ErrNil
|
||||||
|
// other "", error
|
||||||
|
func String(reply interface{}, err error) (string, error) {
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case []byte:
|
||||||
|
return string(reply), nil
|
||||||
|
case string:
|
||||||
|
return reply, nil
|
||||||
|
case nil:
|
||||||
|
return "", ErrNil
|
||||||
|
case Error:
|
||||||
|
return "", reply
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("redigo: unexpected type for String, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes is a helper that converts a command reply to a slice of bytes. If err
|
||||||
|
// is not equal to nil, then Bytes returns nil, err. Otherwise Bytes converts
|
||||||
|
// the reply to a slice of bytes as follows:
|
||||||
|
//
|
||||||
|
// Reply type Result
|
||||||
|
// bulk string reply, nil
|
||||||
|
// simple string []byte(reply), nil
|
||||||
|
// nil nil, ErrNil
|
||||||
|
// other nil, error
|
||||||
|
func Bytes(reply interface{}, err error) ([]byte, error) {
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case []byte:
|
||||||
|
return reply, nil
|
||||||
|
case string:
|
||||||
|
return []byte(reply), nil
|
||||||
|
case nil:
|
||||||
|
return nil, ErrNil
|
||||||
|
case Error:
|
||||||
|
return nil, reply
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("redigo: unexpected type for Bytes, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bool is a helper that converts a command reply to a boolean. If err is not
|
||||||
|
// equal to nil, then Bool returns false, err. Otherwise Bool converts the
|
||||||
|
// reply to boolean as follows:
|
||||||
|
//
|
||||||
|
// Reply type Result
|
||||||
|
// integer value != 0, nil
|
||||||
|
// bulk string strconv.ParseBool(reply)
|
||||||
|
// nil false, ErrNil
|
||||||
|
// other false, error
|
||||||
|
func Bool(reply interface{}, err error) (bool, error) {
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case int64:
|
||||||
|
return reply != 0, nil
|
||||||
|
case []byte:
|
||||||
|
return strconv.ParseBool(string(reply))
|
||||||
|
case nil:
|
||||||
|
return false, ErrNil
|
||||||
|
case Error:
|
||||||
|
return false, reply
|
||||||
|
}
|
||||||
|
return false, fmt.Errorf("redigo: unexpected type for Bool, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiBulk is deprecated. Use Values.
|
||||||
|
func MultiBulk(reply interface{}, err error) ([]interface{}, error) { return Values(reply, err) }
|
||||||
|
|
||||||
|
// Values is a helper that converts an array command reply to a []interface{}.
|
||||||
|
// If err is not equal to nil, then Values returns nil, err. Otherwise, Values
|
||||||
|
// converts the reply as follows:
|
||||||
|
//
|
||||||
|
// Reply type Result
|
||||||
|
// array reply, nil
|
||||||
|
// nil nil, ErrNil
|
||||||
|
// other nil, error
|
||||||
|
func Values(reply interface{}, err error) ([]interface{}, error) {
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
return reply, nil
|
||||||
|
case nil:
|
||||||
|
return nil, ErrNil
|
||||||
|
case Error:
|
||||||
|
return nil, reply
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("redigo: unexpected type for Values, got type %T", reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strings is a helper that converts an array command reply to a []string. If
|
||||||
|
// err is not equal to nil, then Strings returns nil, err. If one of the array
|
||||||
|
// items is not a bulk string or nil, then Strings returns an error.
|
||||||
|
func Strings(reply interface{}, err error) ([]string, error) {
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch reply := reply.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
result := make([]string, len(reply))
|
||||||
|
for i := range reply {
|
||||||
|
if reply[i] == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
p, ok := reply[i].([]byte)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("redigo: unexpected element type for Strings, got type %T", reply[i])
|
||||||
|
}
|
||||||
|
result[i] = string(p)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
case nil:
|
||||||
|
return nil, ErrNil
|
||||||
|
case Error:
|
||||||
|
return nil, reply
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("redigo: unexpected type for Strings, got type %T", reply)
|
||||||
|
}
|
|
@ -0,0 +1,513 @@
|
||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ensureLen(d reflect.Value, n int) {
|
||||||
|
if n > d.Cap() {
|
||||||
|
d.Set(reflect.MakeSlice(d.Type(), n, n))
|
||||||
|
} else {
|
||||||
|
d.SetLen(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cannotConvert(d reflect.Value, s interface{}) error {
|
||||||
|
return fmt.Errorf("redigo: Scan cannot convert from %s to %s",
|
||||||
|
reflect.TypeOf(s), d.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertAssignBytes(d reflect.Value, s []byte) (err error) {
|
||||||
|
switch d.Type().Kind() {
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
var x float64
|
||||||
|
x, err = strconv.ParseFloat(string(s), d.Type().Bits())
|
||||||
|
d.SetFloat(x)
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
var x int64
|
||||||
|
x, err = strconv.ParseInt(string(s), 10, d.Type().Bits())
|
||||||
|
d.SetInt(x)
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
var x uint64
|
||||||
|
x, err = strconv.ParseUint(string(s), 10, d.Type().Bits())
|
||||||
|
d.SetUint(x)
|
||||||
|
case reflect.Bool:
|
||||||
|
var x bool
|
||||||
|
x, err = strconv.ParseBool(string(s))
|
||||||
|
d.SetBool(x)
|
||||||
|
case reflect.String:
|
||||||
|
d.SetString(string(s))
|
||||||
|
case reflect.Slice:
|
||||||
|
if d.Type().Elem().Kind() != reflect.Uint8 {
|
||||||
|
err = cannotConvert(d, s)
|
||||||
|
} else {
|
||||||
|
d.SetBytes(s)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
err = cannotConvert(d, s)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertAssignInt(d reflect.Value, s int64) (err error) {
|
||||||
|
switch d.Type().Kind() {
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
d.SetInt(s)
|
||||||
|
if d.Int() != s {
|
||||||
|
err = strconv.ErrRange
|
||||||
|
d.SetInt(0)
|
||||||
|
}
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
if s < 0 {
|
||||||
|
err = strconv.ErrRange
|
||||||
|
} else {
|
||||||
|
x := uint64(s)
|
||||||
|
d.SetUint(x)
|
||||||
|
if d.Uint() != x {
|
||||||
|
err = strconv.ErrRange
|
||||||
|
d.SetUint(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case reflect.Bool:
|
||||||
|
d.SetBool(s != 0)
|
||||||
|
default:
|
||||||
|
err = cannotConvert(d, s)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertAssignValue(d reflect.Value, s interface{}) (err error) {
|
||||||
|
switch s := s.(type) {
|
||||||
|
case []byte:
|
||||||
|
err = convertAssignBytes(d, s)
|
||||||
|
case int64:
|
||||||
|
err = convertAssignInt(d, s)
|
||||||
|
default:
|
||||||
|
err = cannotConvert(d, s)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertAssignValues(d reflect.Value, s []interface{}) error {
|
||||||
|
if d.Type().Kind() != reflect.Slice {
|
||||||
|
return cannotConvert(d, s)
|
||||||
|
}
|
||||||
|
ensureLen(d, len(s))
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
if err := convertAssignValue(d.Index(i), s[i]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertAssign(d interface{}, s interface{}) (err error) {
|
||||||
|
// Handle the most common destination types using type switches and
|
||||||
|
// fall back to reflection for all other types.
|
||||||
|
switch s := s.(type) {
|
||||||
|
case nil:
|
||||||
|
// ingore
|
||||||
|
case []byte:
|
||||||
|
switch d := d.(type) {
|
||||||
|
case *string:
|
||||||
|
*d = string(s)
|
||||||
|
case *int:
|
||||||
|
*d, err = strconv.Atoi(string(s))
|
||||||
|
case *bool:
|
||||||
|
*d, err = strconv.ParseBool(string(s))
|
||||||
|
case *[]byte:
|
||||||
|
*d = s
|
||||||
|
case *interface{}:
|
||||||
|
*d = s
|
||||||
|
case nil:
|
||||||
|
// skip value
|
||||||
|
default:
|
||||||
|
if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr {
|
||||||
|
err = cannotConvert(d, s)
|
||||||
|
} else {
|
||||||
|
err = convertAssignBytes(d.Elem(), s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case int64:
|
||||||
|
switch d := d.(type) {
|
||||||
|
case *int:
|
||||||
|
x := int(s)
|
||||||
|
if int64(x) != s {
|
||||||
|
err = strconv.ErrRange
|
||||||
|
x = 0
|
||||||
|
}
|
||||||
|
*d = x
|
||||||
|
case *bool:
|
||||||
|
*d = s != 0
|
||||||
|
case *interface{}:
|
||||||
|
*d = s
|
||||||
|
case nil:
|
||||||
|
// skip value
|
||||||
|
default:
|
||||||
|
if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr {
|
||||||
|
err = cannotConvert(d, s)
|
||||||
|
} else {
|
||||||
|
err = convertAssignInt(d.Elem(), s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case []interface{}:
|
||||||
|
switch d := d.(type) {
|
||||||
|
case *[]interface{}:
|
||||||
|
*d = s
|
||||||
|
case *interface{}:
|
||||||
|
*d = s
|
||||||
|
case nil:
|
||||||
|
// skip value
|
||||||
|
default:
|
||||||
|
if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr {
|
||||||
|
err = cannotConvert(d, s)
|
||||||
|
} else {
|
||||||
|
err = convertAssignValues(d.Elem(), s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case Error:
|
||||||
|
err = s
|
||||||
|
default:
|
||||||
|
err = cannotConvert(reflect.ValueOf(d), s)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan copies from src to the values pointed at by dest.
|
||||||
|
//
|
||||||
|
// The values pointed at by dest must be an integer, float, boolean, string,
|
||||||
|
// []byte, interface{} or slices of these types. Scan uses the standard strconv
|
||||||
|
// package to convert bulk strings to numeric and boolean types.
|
||||||
|
//
|
||||||
|
// If a dest value is nil, then the corresponding src value is skipped.
|
||||||
|
//
|
||||||
|
// If a src element is nil, then the corresponding dest value is not modified.
|
||||||
|
//
|
||||||
|
// To enable easy use of Scan in a loop, Scan returns the slice of src
|
||||||
|
// following the copied values.
|
||||||
|
func Scan(src []interface{}, dest ...interface{}) ([]interface{}, error) {
|
||||||
|
if len(src) < len(dest) {
|
||||||
|
return nil, errors.New("redigo: Scan array short")
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
for i, d := range dest {
|
||||||
|
err = convertAssign(d, src[i])
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return src[len(dest):], err
|
||||||
|
}
|
||||||
|
|
||||||
|
type fieldSpec struct {
|
||||||
|
name string
|
||||||
|
index []int
|
||||||
|
//omitEmpty bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type structSpec struct {
|
||||||
|
m map[string]*fieldSpec
|
||||||
|
l []*fieldSpec
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *structSpec) fieldSpec(name []byte) *fieldSpec {
|
||||||
|
return ss.m[string(name)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func compileStructSpec(t reflect.Type, depth map[string]int, index []int, ss *structSpec) {
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
f := t.Field(i)
|
||||||
|
switch {
|
||||||
|
case f.PkgPath != "":
|
||||||
|
// Ignore unexported fields.
|
||||||
|
case f.Anonymous:
|
||||||
|
// TODO: Handle pointers. Requires change to decoder and
|
||||||
|
// protection against infinite recursion.
|
||||||
|
if f.Type.Kind() == reflect.Struct {
|
||||||
|
compileStructSpec(f.Type, depth, append(index, i), ss)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
fs := &fieldSpec{name: f.Name}
|
||||||
|
tag := f.Tag.Get("redis")
|
||||||
|
p := strings.Split(tag, ",")
|
||||||
|
if len(p) > 0 {
|
||||||
|
if p[0] == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(p[0]) > 0 {
|
||||||
|
fs.name = p[0]
|
||||||
|
}
|
||||||
|
for _, s := range p[1:] {
|
||||||
|
switch s {
|
||||||
|
//case "omitempty":
|
||||||
|
// fs.omitempty = true
|
||||||
|
default:
|
||||||
|
panic(errors.New("redigo: unknown field flag " + s + " for type " + t.Name()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d, found := depth[fs.name]
|
||||||
|
if !found {
|
||||||
|
d = 1 << 30
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case len(index) == d:
|
||||||
|
// At same depth, remove from result.
|
||||||
|
delete(ss.m, fs.name)
|
||||||
|
j := 0
|
||||||
|
for i := 0; i < len(ss.l); i++ {
|
||||||
|
if fs.name != ss.l[i].name {
|
||||||
|
ss.l[j] = ss.l[i]
|
||||||
|
j += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ss.l = ss.l[:j]
|
||||||
|
case len(index) < d:
|
||||||
|
fs.index = make([]int, len(index)+1)
|
||||||
|
copy(fs.index, index)
|
||||||
|
fs.index[len(index)] = i
|
||||||
|
depth[fs.name] = len(index)
|
||||||
|
ss.m[fs.name] = fs
|
||||||
|
ss.l = append(ss.l, fs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
structSpecMutex sync.RWMutex
|
||||||
|
structSpecCache = make(map[reflect.Type]*structSpec)
|
||||||
|
defaultFieldSpec = &fieldSpec{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func structSpecForType(t reflect.Type) *structSpec {
|
||||||
|
|
||||||
|
structSpecMutex.RLock()
|
||||||
|
ss, found := structSpecCache[t]
|
||||||
|
structSpecMutex.RUnlock()
|
||||||
|
if found {
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
|
||||||
|
structSpecMutex.Lock()
|
||||||
|
defer structSpecMutex.Unlock()
|
||||||
|
ss, found = structSpecCache[t]
|
||||||
|
if found {
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
|
||||||
|
ss = &structSpec{m: make(map[string]*fieldSpec)}
|
||||||
|
compileStructSpec(t, make(map[string]int), nil, ss)
|
||||||
|
structSpecCache[t] = ss
|
||||||
|
return ss
|
||||||
|
}
|
||||||
|
|
||||||
|
var errScanStructValue = errors.New("redigo: ScanStruct value must be non-nil pointer to a struct")
|
||||||
|
|
||||||
|
// ScanStruct scans alternating names and values from src to a struct. The
|
||||||
|
// HGETALL and CONFIG GET commands return replies in this format.
|
||||||
|
//
|
||||||
|
// ScanStruct uses exported field names to match values in the response. Use
|
||||||
|
// 'redis' field tag to override the name:
|
||||||
|
//
|
||||||
|
// Field int `redis:"myName"`
|
||||||
|
//
|
||||||
|
// Fields with the tag redis:"-" are ignored.
|
||||||
|
//
|
||||||
|
// Integer, float, boolean, string and []byte fields are supported. Scan uses the
|
||||||
|
// standard strconv package to convert bulk string values to numeric and
|
||||||
|
// boolean types.
|
||||||
|
//
|
||||||
|
// If a src element is nil, then the corresponding field is not modified.
|
||||||
|
func ScanStruct(src []interface{}, dest interface{}) error {
|
||||||
|
d := reflect.ValueOf(dest)
|
||||||
|
if d.Kind() != reflect.Ptr || d.IsNil() {
|
||||||
|
return errScanStructValue
|
||||||
|
}
|
||||||
|
d = d.Elem()
|
||||||
|
if d.Kind() != reflect.Struct {
|
||||||
|
return errScanStructValue
|
||||||
|
}
|
||||||
|
ss := structSpecForType(d.Type())
|
||||||
|
|
||||||
|
if len(src)%2 != 0 {
|
||||||
|
return errors.New("redigo: ScanStruct expects even number of values in values")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(src); i += 2 {
|
||||||
|
s := src[i+1]
|
||||||
|
if s == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name, ok := src[i].([]byte)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("redigo: ScanStruct key not a bulk string value")
|
||||||
|
}
|
||||||
|
fs := ss.fieldSpec(name)
|
||||||
|
if fs == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := convertAssignValue(d.FieldByIndex(fs.index), s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
errScanSliceValue = errors.New("redigo: ScanSlice dest must be non-nil pointer to a struct")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ScanSlice scans src to the slice pointed to by dest. The elements the dest
|
||||||
|
// slice must be integer, float, boolean, string, struct or pointer to struct
|
||||||
|
// values.
|
||||||
|
//
|
||||||
|
// Struct fields must be integer, float, boolean or string values. All struct
|
||||||
|
// fields are used unless a subset is specified using fieldNames.
|
||||||
|
func ScanSlice(src []interface{}, dest interface{}, fieldNames ...string) error {
|
||||||
|
d := reflect.ValueOf(dest)
|
||||||
|
if d.Kind() != reflect.Ptr || d.IsNil() {
|
||||||
|
return errScanSliceValue
|
||||||
|
}
|
||||||
|
d = d.Elem()
|
||||||
|
if d.Kind() != reflect.Slice {
|
||||||
|
return errScanSliceValue
|
||||||
|
}
|
||||||
|
|
||||||
|
isPtr := false
|
||||||
|
t := d.Type().Elem()
|
||||||
|
if t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct {
|
||||||
|
isPtr = true
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Kind() != reflect.Struct {
|
||||||
|
ensureLen(d, len(src))
|
||||||
|
for i, s := range src {
|
||||||
|
if s == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := convertAssignValue(d.Index(i), s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ss := structSpecForType(t)
|
||||||
|
fss := ss.l
|
||||||
|
if len(fieldNames) > 0 {
|
||||||
|
fss = make([]*fieldSpec, len(fieldNames))
|
||||||
|
for i, name := range fieldNames {
|
||||||
|
fss[i] = ss.m[name]
|
||||||
|
if fss[i] == nil {
|
||||||
|
return errors.New("redigo: ScanSlice bad field name " + name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fss) == 0 {
|
||||||
|
return errors.New("redigo: ScanSlice no struct fields")
|
||||||
|
}
|
||||||
|
|
||||||
|
n := len(src) / len(fss)
|
||||||
|
if n*len(fss) != len(src) {
|
||||||
|
return errors.New("redigo: ScanSlice length not a multiple of struct field count")
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureLen(d, n)
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
d := d.Index(i)
|
||||||
|
if isPtr {
|
||||||
|
if d.IsNil() {
|
||||||
|
d.Set(reflect.New(t))
|
||||||
|
}
|
||||||
|
d = d.Elem()
|
||||||
|
}
|
||||||
|
for j, fs := range fss {
|
||||||
|
s := src[i*len(fss)+j]
|
||||||
|
if s == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := convertAssignValue(d.FieldByIndex(fs.index), s); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Args is a helper for constructing command arguments from structured values.
|
||||||
|
type Args []interface{}
|
||||||
|
|
||||||
|
// Add returns the result of appending value to args.
|
||||||
|
func (args Args) Add(value ...interface{}) Args {
|
||||||
|
return append(args, value...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFlat returns the result of appending the flattened value of v to args.
|
||||||
|
//
|
||||||
|
// Maps are flattened by appending the alternating keys and map values to args.
|
||||||
|
//
|
||||||
|
// Slices are flattened by appending the slice elements to args.
|
||||||
|
//
|
||||||
|
// Structs are flattened by appending the alternating names and values of
|
||||||
|
// exported fields to args. If v is a nil struct pointer, then nothing is
|
||||||
|
// appended. The 'redis' field tag overrides struct field names. See ScanStruct
|
||||||
|
// for more information on the use of the 'redis' field tag.
|
||||||
|
//
|
||||||
|
// Other types are appended to args as is.
|
||||||
|
func (args Args) AddFlat(v interface{}) Args {
|
||||||
|
rv := reflect.ValueOf(v)
|
||||||
|
switch rv.Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
|
args = flattenStruct(args, rv)
|
||||||
|
case reflect.Slice:
|
||||||
|
for i := 0; i < rv.Len(); i++ {
|
||||||
|
args = append(args, rv.Index(i).Interface())
|
||||||
|
}
|
||||||
|
case reflect.Map:
|
||||||
|
for _, k := range rv.MapKeys() {
|
||||||
|
args = append(args, k.Interface(), rv.MapIndex(k).Interface())
|
||||||
|
}
|
||||||
|
case reflect.Ptr:
|
||||||
|
if rv.Type().Elem().Kind() == reflect.Struct {
|
||||||
|
if !rv.IsNil() {
|
||||||
|
args = flattenStruct(args, rv.Elem())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
args = append(args, v)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
args = append(args, v)
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
func flattenStruct(args Args, v reflect.Value) Args {
|
||||||
|
ss := structSpecForType(v.Type())
|
||||||
|
for _, fs := range ss.l {
|
||||||
|
fv := v.FieldByIndex(fs.index)
|
||||||
|
args = append(args, fs.name, fv.Interface())
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
// Copyright 2012 Gary Burd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License"): you may
|
||||||
|
// not use this file except in compliance with the License. You may obtain
|
||||||
|
// a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
// License for the specific language governing permissions and limitations
|
||||||
|
// under the License.
|
||||||
|
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Script encapsulates the source, hash and key count for a Lua script. See
|
||||||
|
// http://redis.io/commands/eval for information on scripts in Redis.
|
||||||
|
type Script struct {
|
||||||
|
keyCount int
|
||||||
|
src string
|
||||||
|
hash string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewScript returns a new script object. If keyCount is greater than or equal
|
||||||
|
// to zero, then the count is automatically inserted in the EVAL command
|
||||||
|
// argument list. If keyCount is less than zero, then the application supplies
|
||||||
|
// the count as the first value in the keysAndArgs argument to the Do, Send and
|
||||||
|
// SendHash methods.
|
||||||
|
func NewScript(keyCount int, src string) *Script {
|
||||||
|
h := sha1.New()
|
||||||
|
io.WriteString(h, src)
|
||||||
|
return &Script{keyCount, src, hex.EncodeToString(h.Sum(nil))}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Script) args(spec string, keysAndArgs []interface{}) []interface{} {
|
||||||
|
var args []interface{}
|
||||||
|
if s.keyCount < 0 {
|
||||||
|
args = make([]interface{}, 1+len(keysAndArgs))
|
||||||
|
args[0] = spec
|
||||||
|
copy(args[1:], keysAndArgs)
|
||||||
|
} else {
|
||||||
|
args = make([]interface{}, 2+len(keysAndArgs))
|
||||||
|
args[0] = spec
|
||||||
|
args[1] = s.keyCount
|
||||||
|
copy(args[2:], keysAndArgs)
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do evalutes the script. Under the covers, Do optimistically evaluates the
|
||||||
|
// script using the EVALSHA command. If the command fails because the script is
|
||||||
|
// not loaded, then Do evaluates the script using the EVAL command (thus
|
||||||
|
// causing the script to load).
|
||||||
|
func (s *Script) Do(c Conn, keysAndArgs ...interface{}) (interface{}, error) {
|
||||||
|
v, err := c.Do("EVALSHA", s.args(s.hash, keysAndArgs)...)
|
||||||
|
if e, ok := err.(Error); ok && strings.HasPrefix(string(e), "NOSCRIPT ") {
|
||||||
|
v, err = c.Do("EVAL", s.args(s.src, keysAndArgs)...)
|
||||||
|
}
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendHash evaluates the script without waiting for the reply. The script is
|
||||||
|
// evaluated with the EVALSHA command. The application must ensure that the
|
||||||
|
// script is loaded by a previous call to Send, Do or Load methods.
|
||||||
|
func (s *Script) SendHash(c Conn, keysAndArgs ...interface{}) error {
|
||||||
|
return c.Send("EVALSHA", s.args(s.hash, keysAndArgs)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send evaluates the script without waiting for the reply.
|
||||||
|
func (s *Script) Send(c Conn, keysAndArgs ...interface{}) error {
|
||||||
|
return c.Send("EVAL", s.args(s.src, keysAndArgs)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load loads the script without evaluating it.
|
||||||
|
func (s *Script) Load(c Conn) error {
|
||||||
|
_, err := c.Do("SCRIPT", "LOAD", s.src)
|
||||||
|
return err
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/garyburd/redigo/redis"
|
"github.com/siddontang/ledisdb/client/go/redis"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
|
@ -2,7 +2,7 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/garyburd/redigo/redis"
|
"github.com/siddontang/ledisdb/client/go/redis"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/garyburd/redigo/redis"
|
"github.com/siddontang/ledisdb/client/go/redis"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/garyburd/redigo/redis"
|
"github.com/siddontang/ledisdb/client/go/redis"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/garyburd/redigo/redis"
|
"github.com/siddontang/ledisdb/client/go/redis"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,7 +2,7 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/garyburd/redigo/redis"
|
"github.com/siddontang/ledisdb/client/go/redis"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue