diff --git a/client/go/ledis-cli/main.go b/client/go/ledis-cli/main.go new file mode 100644 index 0000000..5cb94df --- /dev/null +++ b/client/go/ledis-cli/main.go @@ -0,0 +1,81 @@ +package main + +import ( + "bufio" + "flag" + "fmt" + "github.com/siddontang/ledisdb/client/go/ledis" + "os" + "strings" +) + +var ip = flag.String("h", "127.0.0.1", "ledisdb server ip (default 127.0.0.1)") +var port = flag.Int("p", 6380, "ledisdb server port (default 6380)") +var socket = flag.String("s", "", "ledisdb server socket, overwrite ip and port") + +func main() { + flag.Parse() + + cfg := new(ledis.Config) + if len(*socket) > 0 { + cfg.Addr = *socket + } else { + cfg.Addr = fmt.Sprintf("%s:%d", *ip, *port) + } + + cfg.MaxIdleConns = 1 + + c := ledis.NewClient(cfg) + + reader := bufio.NewReader(os.Stdin) + + for { + fmt.Printf("ledis %s > ", cfg.Addr) + + cmd, _ := reader.ReadString('\n') + + cmds := strings.Fields(cmd) + if len(cmds) == 0 { + continue + } else { + args := make([]interface{}, len(cmds[1:])) + for i := range args { + args[i] = cmds[1+i] + } + r, err := c.Do(cmds[0], args...) + if err != nil { + fmt.Printf("%s", err.Error()) + } else { + printReply(r) + } + + fmt.Printf("\n") + } + } +} + +func printReply(reply interface{}) { + switch reply := reply.(type) { + case int64: + fmt.Printf("(integer) %d", reply) + case string: + fmt.Printf("%q", reply) + case []byte: + fmt.Printf("%q", reply) + case nil: + fmt.Printf("(empty list or set)") + case ledis.Error: + fmt.Printf("%s", string(reply)) + case []interface{}: + for i, v := range reply { + fmt.Printf("%d) ", i) + if v == nil { + fmt.Printf("(nil)") + } else { + fmt.Printf("%q", v) + } + } + default: + fmt.Printf("invalid ledis reply") + } +} diff --git a/client/go/ledis/client.go b/client/go/ledis/client.go new file mode 100644 index 0000000..dfe35b8 --- /dev/null +++ b/client/go/ledis/client.go @@ -0,0 +1,96 @@ +package ledis + +import ( + "container/list" + "strings" + "sync" + "time" +) + +const ( + pingPeriod time.Duration = 3 * time.Second +) + +type Config struct { + Addr string + MaxIdleConns int +} + +type Client struct { + sync.Mutex + + cfg *Config + proto string + + conns *list.List +} + +func NewClient(cfg *Config) *Client { + c := new(Client) + + c.cfg = cfg + + if strings.Contains(cfg.Addr, "/") { + c.proto = "unix" + } else { + c.proto = "tcp" + } + + c.conns = list.New() + + return c +} + +func (c *Client) Do(cmd string, args ...interface{}) (interface{}, error) { + co := c.get() + r, err := co.Do(cmd, args...) + c.put(co) + + return r, err +} + +func (c *Client) Close() { + c.Lock() + defer c.Unlock() + + for c.conns.Len() > 0 { + e := c.conns.Front() + co := e.Value.(*Conn) + c.conns.Remove(e) + + co.finalize() + } +} + +func (c *Client) Get() *Conn { + return c.get() +} + +func (c *Client) get() *Conn { + c.Lock() + if c.conns.Len() == 0 { + c.Unlock() + + return c.newConn() + } else { + e := c.conns.Front() + co := e.Value.(*Conn) + c.conns.Remove(e) + + c.Unlock() + + return co + } +} + +func (c *Client) put(conn *Conn) { + c.Lock() + if c.conns.Len() >= c.cfg.MaxIdleConns { + c.Unlock() + conn.finalize() + } else { + conn.lastActive = time.Now() + c.conns.PushFront(conn) + c.Unlock() + } +} diff --git a/client/go/ledis/conn.go b/client/go/ledis/conn.go new file mode 100644 index 0000000..688460e --- /dev/null +++ b/client/go/ledis/conn.go @@ -0,0 +1,309 @@ +package ledis + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "net" + "strconv" + "time" +) + +// Error represents an error returned in a command reply. +type Error string + +func (err Error) Error() string { return string(err) } + +type Conn struct { + client *Client + + c net.Conn + br *bufio.Reader + bw *bufio.Writer + + lastActive time.Time + + // Scratch space for formatting argument length. + // '*' or '$', length, "\r\n" + lenScratch [32]byte + + // Scratch space for formatting integers and floats. + numScratch [40]byte +} + +func (c *Conn) Close() { + c.client.put(c) +} + +func (c *Conn) Do(cmd string, args ...interface{}) (interface{}, error) { + if err := c.connect(); err != nil { + return nil, err + } + + if err := c.writeCommand(cmd, args); err != nil { + c.finalize() + return nil, err + } + + if err := c.bw.Flush(); err != nil { + c.finalize() + return nil, err + } + + if reply, err := c.readReply(); err != nil { + c.finalize() + return nil, err + } else { + if e, ok := reply.(Error); ok { + return reply, e + } else { + return reply, nil + } + } +} + +func (c *Conn) finalize() { + if c.c != nil { + c.c.Close() + c.c = nil + } +} + +func (c *Conn) connect() error { + if c.c != nil { + return nil + } + + var err error + c.c, err = net.Dial(c.client.proto, c.client.cfg.Addr) + if err != nil { + return err + } + + if c.br != nil { + c.br.Reset(c.c) + } else { + c.br = bufio.NewReader(c.c) + } + + if c.bw != nil { + c.bw.Reset(c.c) + } else { + c.bw = bufio.NewWriter(c.c) + } + + return nil +} + +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("ledis: long response line") + } + if err != nil { + return nil, err + } + i := len(p) - 2 + if i < 0 || p[i] != '\r' { + return nil, errors.New("ledis: 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("ledis: 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("ledis: 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("ledis: malformed integer") + } + + var negate bool + if p[0] == '-' { + negate = true + p = p[1:] + if len(p) == 0 { + return 0, errors.New("ledis: malformed integer") + } + } + + var n int64 + for _, b := range p { + n *= 10 + if b < '0' || b > '9' { + return 0, errors.New("ledis: 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("ledis: 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("ledis: 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("ledis: unexpected response line") +} + +func (c *Client) newConn() *Conn { + co := new(Conn) + co.client = c + + return co +} diff --git a/client/go/ledis/garyburd_license b/client/go/ledis/garyburd_license new file mode 100644 index 0000000..8867881 --- /dev/null +++ b/client/go/ledis/garyburd_license @@ -0,0 +1,13 @@ +// 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. \ No newline at end of file diff --git a/client/go/ledis/ledis_test.go b/client/go/ledis/ledis_test.go new file mode 100644 index 0000000..6582d04 --- /dev/null +++ b/client/go/ledis/ledis_test.go @@ -0,0 +1,15 @@ +package ledis + +import ( + "testing" +) + +func TestClient(t *testing.T) { + cfg := new(Config) + cfg.Addr = "127.0.0.1:6380" + cfg.MaxIdleConns = 4 + + c := NewClient(cfg) + + c.Close() +} diff --git a/client/go/ledis/reply.go b/client/go/ledis/reply.go new file mode 100644 index 0000000..e9e8993 --- /dev/null +++ b/client/go/ledis/reply.go @@ -0,0 +1,257 @@ +package ledis + +import ( + "errors" + "fmt" + "strconv" +) + +// ErrNil indicates that a reply value is nil. +var ErrNil = errors.New("ledis: 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("ledis: 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("ledis: unexpected type for Int64, got type %T", reply) +} + +var errNegativeInt = errors.New("ledis: 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("ledis: 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("ledis: 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("ledis: 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("ledis: 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("ledis: 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("ledis: 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("ledis: 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("ledis: unexpected type for Strings, got type %T", reply) +}