├── bench ├── bench.png ├── asyncbench │ ├── asynclient.go │ └── asyncbench.go ├── syncbench │ ├── synclient.go │ └── syncbench.go └── benchresult.txt ├── .travis.yml ├── .gitignore ├── redisx ├── doc.go ├── connmux.go └── connmux_test.go ├── internal ├── commandinfo_test.go ├── commandinfo.go └── redistest │ └── testdb.go ├── redis ├── pre_go17.go ├── go17.go ├── redis.go ├── zpop_example_test.go ├── script_test.go ├── script.go ├── log.go ├── pubsub.go ├── pubsub_test.go ├── reply_test.go ├── test_test.go ├── asyncpool.go ├── asynconn.go ├── doc.go ├── pool.go ├── scan_test.go ├── reply.go ├── pool_test.go ├── conn.go ├── scan.go └── conn_test.go ├── README.markdown ├── example └── example.go └── LICENSE /bench/bench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gistao/RedisGo-Async/HEAD/bench/bench.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | services: 4 | - redis-server 5 | 6 | go: 7 | - 1.4 8 | - 1.5 9 | - 1.6 10 | - 1.7 11 | - 1.8 12 | - tip 13 | 14 | script: 15 | - go get -t -v ./... 16 | - diff -u <(echo -n) <(gofmt -d .) 17 | - go vet $(go list ./... | grep -v /vendor/) 18 | - go test -v -race ./... 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | -------------------------------------------------------------------------------- /redisx/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Gary Burd 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | // Package redisx contains experimental features for Redigo. Features in this 16 | // package may be modified or deleted at any time. 17 | package redisx 18 | -------------------------------------------------------------------------------- /internal/commandinfo_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "testing" 4 | 5 | func TestLookupCommandInfo(t *testing.T) { 6 | for _, n := range []string{"watch", "WATCH", "wAtch"} { 7 | if LookupCommandInfo(n) == (CommandInfo{}) { 8 | t.Errorf("LookupCommandInfo(%q) = CommandInfo{}, expected non-zero value", n) 9 | } 10 | } 11 | } 12 | 13 | func benchmarkLookupCommandInfo(b *testing.B, names ...string) { 14 | for i := 0; i < b.N; i++ { 15 | for _, c := range names { 16 | LookupCommandInfo(c) 17 | } 18 | } 19 | } 20 | 21 | func BenchmarkLookupCommandInfoCorrectCase(b *testing.B) { 22 | benchmarkLookupCommandInfo(b, "watch", "WATCH", "monitor", "MONITOR") 23 | } 24 | 25 | func BenchmarkLookupCommandInfoMixedCase(b *testing.B) { 26 | benchmarkLookupCommandInfo(b, "wAtch", "WeTCH", "monItor", "MONiTOR") 27 | } 28 | -------------------------------------------------------------------------------- /redis/pre_go17.go: -------------------------------------------------------------------------------- 1 | // +build !go1.7 2 | 3 | package redis 4 | 5 | import "crypto/tls" 6 | 7 | // similar cloneTLSClientConfig in the stdlib, but also honor skipVerify for the nil case 8 | func cloneTLSClientConfig(cfg *tls.Config, skipVerify bool) *tls.Config { 9 | if cfg == nil { 10 | return &tls.Config{InsecureSkipVerify: skipVerify} 11 | } 12 | return &tls.Config{ 13 | Rand: cfg.Rand, 14 | Time: cfg.Time, 15 | Certificates: cfg.Certificates, 16 | NameToCertificate: cfg.NameToCertificate, 17 | GetCertificate: cfg.GetCertificate, 18 | RootCAs: cfg.RootCAs, 19 | NextProtos: cfg.NextProtos, 20 | ServerName: cfg.ServerName, 21 | ClientAuth: cfg.ClientAuth, 22 | ClientCAs: cfg.ClientCAs, 23 | InsecureSkipVerify: cfg.InsecureSkipVerify, 24 | CipherSuites: cfg.CipherSuites, 25 | PreferServerCipherSuites: cfg.PreferServerCipherSuites, 26 | ClientSessionCache: cfg.ClientSessionCache, 27 | MinVersion: cfg.MinVersion, 28 | MaxVersion: cfg.MaxVersion, 29 | CurvePreferences: cfg.CurvePreferences, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /redis/go17.go: -------------------------------------------------------------------------------- 1 | // +build go1.7 2 | 3 | package redis 4 | 5 | import "crypto/tls" 6 | 7 | // similar cloneTLSClientConfig in the stdlib, but also honor skipVerify for the nil case 8 | func cloneTLSClientConfig(cfg *tls.Config, skipVerify bool) *tls.Config { 9 | if cfg == nil { 10 | return &tls.Config{InsecureSkipVerify: skipVerify} 11 | } 12 | return &tls.Config{ 13 | Rand: cfg.Rand, 14 | Time: cfg.Time, 15 | Certificates: cfg.Certificates, 16 | NameToCertificate: cfg.NameToCertificate, 17 | GetCertificate: cfg.GetCertificate, 18 | RootCAs: cfg.RootCAs, 19 | NextProtos: cfg.NextProtos, 20 | ServerName: cfg.ServerName, 21 | ClientAuth: cfg.ClientAuth, 22 | ClientCAs: cfg.ClientCAs, 23 | InsecureSkipVerify: cfg.InsecureSkipVerify, 24 | CipherSuites: cfg.CipherSuites, 25 | PreferServerCipherSuites: cfg.PreferServerCipherSuites, 26 | ClientSessionCache: cfg.ClientSessionCache, 27 | MinVersion: cfg.MinVersion, 28 | MaxVersion: cfg.MaxVersion, 29 | CurvePreferences: cfg.CurvePreferences, 30 | DynamicRecordSizingDisabled: cfg.DynamicRecordSizingDisabled, 31 | Renegotiation: cfg.Renegotiation, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /internal/commandinfo.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Gary Burd 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package internal 16 | 17 | import ( 18 | "strings" 19 | ) 20 | 21 | const ( 22 | WatchState = 1 << iota 23 | MultiState 24 | SubscribeState 25 | MonitorState 26 | ) 27 | 28 | type CommandInfo struct { 29 | Set, Clear int 30 | } 31 | 32 | var commandInfos = map[string]CommandInfo{ 33 | "WATCH": {Set: WatchState}, 34 | "UNWATCH": {Clear: WatchState}, 35 | "MULTI": {Set: MultiState}, 36 | "EXEC": {Clear: WatchState | MultiState}, 37 | "DISCARD": {Clear: WatchState | MultiState}, 38 | "PSUBSCRIBE": {Set: SubscribeState}, 39 | "SUBSCRIBE": {Set: SubscribeState}, 40 | "MONITOR": {Set: MonitorState}, 41 | } 42 | 43 | func init() { 44 | for n, ci := range commandInfos { 45 | commandInfos[strings.ToLower(n)] = ci 46 | } 47 | } 48 | 49 | func LookupCommandInfo(commandName string) CommandInfo { 50 | if ci, ok := commandInfos[commandName]; ok { 51 | return ci 52 | } 53 | return commandInfos[strings.ToUpper(commandName)] 54 | } 55 | -------------------------------------------------------------------------------- /internal/redistest/testdb.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Gary Burd 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | // Package redistest contains utilities for writing Redigo tests. 16 | package redistest 17 | 18 | import ( 19 | "errors" 20 | "time" 21 | 22 | "github.com/gistao/RedisGo-Async/redis" 23 | ) 24 | 25 | type testConn struct { 26 | redis.Conn 27 | } 28 | 29 | func (t testConn) Close() error { 30 | _, err := t.Conn.Do("SELECT", "9") 31 | if err != nil { 32 | return nil 33 | } 34 | _, err = t.Conn.Do("FLUSHDB") 35 | if err != nil { 36 | return err 37 | } 38 | return t.Conn.Close() 39 | } 40 | 41 | // Dial dials the local Redis server and selects database 9. To prevent 42 | // stomping on real data, DialTestDB fails if database 9 contains data. The 43 | // returned connection flushes database 9 on close. 44 | func Dial() (redis.Conn, error) { 45 | c, err := redis.DialTimeout("tcp", ":6379", 0, 1*time.Second, 1*time.Second) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | _, err = c.Do("SELECT", "9") 51 | if err != nil { 52 | c.Close() 53 | return nil, err 54 | } 55 | 56 | n, err := redis.Int(c.Do("DBSIZE")) 57 | if err != nil { 58 | c.Close() 59 | return nil, err 60 | } 61 | 62 | if n != 0 { 63 | c.Close() 64 | return nil, errors.New("database #9 is not empty, test can not continue") 65 | } 66 | 67 | return testConn{c}, nil 68 | } 69 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | ## RedisGo-Async 2 | 3 | [![GoDoc](http://godoc.org/github.com/gistao/RedisGo-Async/redis?status.svg)](http://godoc.org/github.com/gistao/RedisGo-Async/redis) 4 | 5 | ## Features 6 | ### asynchronous(only one connection) 7 | * A [Print-like](http://godoc.org/github.com/gistao/RedisGo-Async/redis#hdr-Executing_Commands) API with support for all Redis commands. 8 | 9 | * [Pipelining](http://godoc.org/github.com/gistao/RedisGo-Async/redis#hdr-Pipelining) 10 | 11 | * [Script helper type](http://godoc.org/github.com/gistao/RedisGo-Async/redis#Script) with optimistic use of EVALSHA. 12 | 13 | * Transactions limited support. 14 | 15 | * [Helper functions](http://godoc.org/github.com/gistao/RedisGo-Async/redis#hdr-Reply_Helpers) for working with command replies. 16 | 17 | 18 | ### synchronous(connection pool) 19 | * [Print-like](http://godoc.org/github.com/gistao/RedisGo-Async/redis#hdr-Executing_Commands) API with support for all Redis commands. 20 | 21 | * [Pipelining](http://godoc.org/github.com/gistao/RedisGo-Async/redis#hdr-Pipelining) 22 | 23 | * [Publish/Subscribe](http://godoc.org/github.com/gistao/RedisGo-Async/redis#hdr-Publish_and_Subscribe) 24 | 25 | * [Script helper type](http://godoc.org/github.com/gistao/RedisGo-Async/redis#Script) with optimistic use of EVALSHA. 26 | 27 | * [Helper functions](http://godoc.org/github.com/gistao/RedisGo-Async/redis#hdr-Reply_Helpers) for working with command replies. 28 | 29 | 30 | ## Installation 31 | 32 | Install RedisGo-Async using the "go get" command: 33 | 34 | go get github.com/gistao/RedisGo-Async/redis 35 | 36 | The Go distribution is RedisGo-Async's only dependency. 37 | 38 | 39 | ## Benchmark Test 40 | ![bench report](https://github.com/gistao/RedisGo-Async/blob/master/bench/bench.png "Title") 41 | 42 | 43 | ## Related Projects 44 | - [garyburd/redigo](https://github.com/garyburd/redigo) - A client library for Redis. 45 | 46 | 47 | ## Contributing 48 | * garyburd(https://github.com/garyburd). 49 | * xiaofei(https://github.com/feibulei). 50 | 51 | 52 | License 53 | ------- 54 | 55 | RedisGo-Async is available under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). 56 | -------------------------------------------------------------------------------- /bench/asyncbench/asynclient.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 xiaofei, gistao 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "github.com/gistao/RedisGo-Async/redis" 19 | "sync" 20 | ) 21 | 22 | type AsynClient struct { 23 | pool *redis.AsyncPool 24 | Addr string 25 | } 26 | 27 | var ( 28 | asyncMap map[string]*AsynClient 29 | asyncMutex *sync.RWMutex 30 | ) 31 | 32 | const ( 33 | defaultTimeout = 300 34 | ) 35 | 36 | func init() { 37 | asyncMap = make(map[string]*AsynClient) 38 | asyncMutex = new(sync.RWMutex) 39 | } 40 | 41 | func newAsyncPool(addr string) *redis.AsyncPool { 42 | return &redis.AsyncPool{ 43 | Dial: func() (redis.AsynConn, error) { 44 | c, err := redis.AsyncDial("tcp", addr) 45 | return c, err 46 | }, 47 | } 48 | } 49 | 50 | func GetAsynClient(addr string) *AsynClient { 51 | var redis *AsynClient 52 | var mok bool 53 | asyncMutex.RLock() 54 | redis, mok = asyncMap[addr] 55 | asyncMutex.RUnlock() 56 | if !mok { 57 | asyncMutex.Lock() 58 | redis, mok = asyncMap[addr] 59 | if !mok { 60 | redis = &AsynClient{pool: newAsyncPool(addr), Addr: addr} 61 | asyncMap[addr] = redis 62 | } 63 | asyncMutex.Unlock() 64 | } 65 | return redis 66 | } 67 | 68 | func (c *AsynClient) Get(key string) (string, error) { 69 | conn := c.pool.Get() 70 | defer conn.Close() 71 | reply, errDo := conn.Do("GET", key) 72 | if errDo == nil && reply == nil { 73 | return "", nil 74 | } 75 | val, err := redis.String(reply, errDo) 76 | return val, err 77 | } 78 | 79 | func (c *AsynClient) Del(key string) (int64, error) { 80 | conn := c.pool.Get() 81 | defer conn.Close() 82 | reply, errDo := conn.Do("DEL", key) 83 | if errDo == nil && reply == nil { 84 | return 0, nil 85 | } 86 | val, err := redis.Int64(reply, errDo) 87 | return val, err 88 | } 89 | 90 | func (c *AsynClient) Set(key string, val string) (string, error) { 91 | conn := c.pool.Get() 92 | defer conn.Close() 93 | val, err := redis.String(conn.Do("SET", key, val)) 94 | return val, err 95 | } 96 | -------------------------------------------------------------------------------- /redis/redis.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Gary Burd 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package redis 16 | 17 | // Error represents an error returned in a command reply. 18 | type Error string 19 | 20 | func (err Error) Error() string { return string(err) } 21 | 22 | // Conn represents a connection to a Redis server. 23 | type Conn interface { 24 | // Close closes the connection. 25 | Close() error 26 | 27 | // Err returns a non-nil value when the connection is not usable. 28 | Err() error 29 | 30 | // Do sends a command to the server and returns the received reply. 31 | Do(commandName string, args ...interface{}) (reply interface{}, err error) 32 | 33 | // Send writes the command to the client's output buffer. 34 | Send(commandName string, args ...interface{}) error 35 | 36 | // Flush flushes the output buffer to the Redis server. 37 | Flush() error 38 | 39 | // Receive receives a single reply from the Redis server 40 | Receive() (reply interface{}, err error) 41 | } 42 | 43 | // AsyncRet represents a return value of AsyncDo 44 | type AsyncRet interface { 45 | // Get get a command result asynchronously 46 | Get() (reply interface{}, err error) 47 | } 48 | 49 | // AsynConn represents a aync connection to a Redis server. 50 | type AsynConn interface { 51 | Conn 52 | // Do sends a command to the server and returns the received reply. 53 | AsyncDo(commandName string, args ...interface{}) (ret AsyncRet, err error) 54 | } 55 | 56 | // Argument is implemented by types which want to control how their value is 57 | // interpreted when used as an argument to a redis command. 58 | type Argument interface { 59 | // RedisArg returns the interface that represents the value to be used 60 | // in redis commands. 61 | RedisArg() interface{} 62 | } 63 | 64 | // Scanner is implemented by types which want to control how their value is 65 | // interpreted when read from redis. 66 | type Scanner interface { 67 | // RedisScan assigns a value from a redis value. 68 | // 69 | // An error should be returned if the value cannot be stored without 70 | // loss of information. 71 | RedisScan(src interface{}) error 72 | } 73 | -------------------------------------------------------------------------------- /bench/syncbench/synclient.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 xiaofei, gistao 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "github.com/gistao/RedisGo-Async/redis" 19 | "sync" 20 | "time" 21 | ) 22 | 23 | type SynClient struct { 24 | pool *redis.Pool 25 | Addr string 26 | } 27 | 28 | var ( 29 | syncMap map[string]*SynClient 30 | syncMutex *sync.RWMutex 31 | ) 32 | 33 | const ( 34 | defaultTimeout = 300 35 | ) 36 | 37 | func init() { 38 | syncMap = make(map[string]*SynClient) 39 | syncMutex = new(sync.RWMutex) 40 | } 41 | 42 | func newSyncPool(addr string) *redis.Pool { 43 | return &redis.Pool{ 44 | MaxIdle: 100, 45 | MaxActive: 100, 46 | IdleTimeout: time.Minute * 1, 47 | Wait: true, 48 | Dial: func() (redis.Conn, error) { 49 | c, err := redis.Dial("tcp", addr) 50 | return c, err 51 | }, 52 | } 53 | } 54 | 55 | func GetSynClient(addr string) *SynClient { 56 | var redis *SynClient 57 | var mok bool 58 | syncMutex.RLock() 59 | redis, mok = syncMap[addr] 60 | syncMutex.RUnlock() 61 | if !mok { 62 | syncMutex.Lock() 63 | redis, mok = syncMap[addr] 64 | if !mok { 65 | redis = &SynClient{pool: newSyncPool(addr), Addr: addr} 66 | syncMap[addr] = redis 67 | } 68 | syncMutex.Unlock() 69 | } 70 | return redis 71 | } 72 | 73 | func (c *SynClient) Get(key string) (string, error) { 74 | conn := c.pool.Get() 75 | defer conn.Close() 76 | reply, errDo := conn.Do("GET", key) 77 | if errDo == nil && reply == nil { 78 | return "", nil 79 | } 80 | val, err := redis.String(reply, errDo) 81 | return val, err 82 | } 83 | 84 | func (c *SynClient) Del(key string) (int64, error) { 85 | conn := c.pool.Get() 86 | defer conn.Close() 87 | reply, errDo := conn.Do("DEL", key) 88 | if errDo == nil && reply == nil { 89 | return 0, nil 90 | } 91 | val, err := redis.Int64(reply, errDo) 92 | return val, err 93 | } 94 | 95 | func (c *SynClient) Set(key string, val string) (string, error) { 96 | conn := c.pool.Get() 97 | defer conn.Close() 98 | val, err := redis.String(conn.Do("SET", key, val)) 99 | return val, err 100 | } 101 | -------------------------------------------------------------------------------- /redis/zpop_example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Gary Burd 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package redis_test 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/gistao/RedisGo-Async/redis" 21 | ) 22 | 23 | // zpop pops a value from the ZSET key using WATCH/MULTI/EXEC commands. 24 | func zpop(c redis.Conn, key string) (result string, err error) { 25 | 26 | defer func() { 27 | // Return connection to normal state on error. 28 | if err != nil { 29 | c.Do("DISCARD") 30 | } 31 | }() 32 | 33 | // Loop until transaction is successful. 34 | for { 35 | if _, err := c.Do("WATCH", key); err != nil { 36 | return "", err 37 | } 38 | 39 | members, err := redis.Strings(c.Do("ZRANGE", key, 0, 0)) 40 | if err != nil { 41 | return "", err 42 | } 43 | if len(members) != 1 { 44 | return "", redis.ErrNil 45 | } 46 | 47 | c.Send("MULTI") 48 | c.Send("ZREM", key, members[0]) 49 | queued, err := c.Do("EXEC") 50 | if err != nil { 51 | return "", err 52 | } 53 | 54 | if queued != nil { 55 | result = members[0] 56 | break 57 | } 58 | } 59 | 60 | return result, nil 61 | } 62 | 63 | // zpopScript pops a value from a ZSET. 64 | var zpopScript = redis.NewScript(1, ` 65 | local r = redis.call('ZRANGE', KEYS[1], 0, 0) 66 | if r ~= nil then 67 | r = r[1] 68 | redis.call('ZREM', KEYS[1], r) 69 | end 70 | return r 71 | `) 72 | 73 | // This example implements ZPOP as described at 74 | // http://redis.io/topics/transactions using WATCH/MULTI/EXEC and scripting. 75 | func Example_zpop() { 76 | c, err := dial() 77 | if err != nil { 78 | fmt.Println(err) 79 | return 80 | } 81 | defer c.Close() 82 | 83 | // Add test data using a pipeline. 84 | 85 | for i, member := range []string{"red", "blue", "green"} { 86 | c.Send("ZADD", "zset", i, member) 87 | } 88 | if _, err := c.Do(""); err != nil { 89 | fmt.Println(err) 90 | return 91 | } 92 | 93 | // Pop using WATCH/MULTI/EXEC 94 | 95 | v, err := zpop(c, "zset") 96 | if err != nil { 97 | fmt.Println(err) 98 | return 99 | } 100 | fmt.Println(v) 101 | 102 | // Pop using a script. 103 | 104 | v, err = redis.String(zpopScript.Do(c, "zset")) 105 | if err != nil { 106 | fmt.Println(err) 107 | return 108 | } 109 | fmt.Println(v) 110 | 111 | // Output: 112 | // red 113 | // blue 114 | } 115 | -------------------------------------------------------------------------------- /redis/script_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Gary Burd 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package redis_test 16 | 17 | import ( 18 | "fmt" 19 | "reflect" 20 | "testing" 21 | "time" 22 | 23 | "github.com/gistao/RedisGo-Async/redis" 24 | ) 25 | 26 | var ( 27 | // These variables are declared at package level to remove distracting 28 | // details from the examples. 29 | c redis.Conn 30 | reply interface{} 31 | err error 32 | ) 33 | 34 | func ExampleScript() { 35 | // Initialize a package-level variable with a script. 36 | var getScript = redis.NewScript(1, `return redis.call('get', KEYS[1])`) 37 | 38 | // In a function, use the script Do method to evaluate the script. The Do 39 | // method optimistically uses the EVALSHA command. If the script is not 40 | // loaded, then the Do method falls back to the EVAL command. 41 | reply, err = getScript.Do(c, "foo") 42 | } 43 | 44 | func TestScript(t *testing.T) { 45 | c, err := redis.DialDefaultServer() 46 | if err != nil { 47 | t.Fatalf("error connection to database, %v", err) 48 | } 49 | defer c.Close() 50 | 51 | // To test fall back in Do, we make script unique by adding comment with current time. 52 | script := fmt.Sprintf("--%d\nreturn {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", time.Now().UnixNano()) 53 | s := redis.NewScript(2, script) 54 | reply := []interface{}{[]byte("key1"), []byte("key2"), []byte("arg1"), []byte("arg2")} 55 | 56 | v, err := s.Do(c, "key1", "key2", "arg1", "arg2") 57 | if err != nil { 58 | t.Errorf("s.Do(c, ...) returned %v", err) 59 | } 60 | 61 | if !reflect.DeepEqual(v, reply) { 62 | t.Errorf("s.Do(c, ..); = %v, want %v", v, reply) 63 | } 64 | 65 | err = s.Load(c) 66 | if err != nil { 67 | t.Errorf("s.Load(c) returned %v", err) 68 | } 69 | 70 | err = s.SendHash(c, "key1", "key2", "arg1", "arg2") 71 | if err != nil { 72 | t.Errorf("s.SendHash(c, ...) returned %v", err) 73 | } 74 | 75 | err = c.Flush() 76 | if err != nil { 77 | t.Errorf("c.Flush() returned %v", err) 78 | } 79 | 80 | v, err = c.Receive() 81 | if !reflect.DeepEqual(v, reply) { 82 | t.Errorf("s.SendHash(c, ..); c.Receive() = %v, want %v", v, reply) 83 | } 84 | 85 | err = s.Send(c, "key1", "key2", "arg1", "arg2") 86 | if err != nil { 87 | t.Errorf("s.Send(c, ...) returned %v", err) 88 | } 89 | 90 | err = c.Flush() 91 | if err != nil { 92 | t.Errorf("c.Flush() returned %v", err) 93 | } 94 | 95 | v, err = c.Receive() 96 | if !reflect.DeepEqual(v, reply) { 97 | t.Errorf("s.Send(c, ..); c.Receive() = %v, want %v", v, reply) 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /redis/script.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Gary Burd 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package redis 16 | 17 | import ( 18 | "crypto/sha1" 19 | "encoding/hex" 20 | "io" 21 | "strings" 22 | ) 23 | 24 | // Script encapsulates the source, hash and key count for a Lua script. See 25 | // http://redis.io/commands/eval for information on scripts in Redis. 26 | type Script struct { 27 | keyCount int 28 | src string 29 | hash string 30 | } 31 | 32 | // NewScript returns a new script object. If keyCount is greater than or equal 33 | // to zero, then the count is automatically inserted in the EVAL command 34 | // argument list. If keyCount is less than zero, then the application supplies 35 | // the count as the first value in the keysAndArgs argument to the Do, Send and 36 | // SendHash methods. 37 | func NewScript(keyCount int, src string) *Script { 38 | h := sha1.New() 39 | io.WriteString(h, src) 40 | return &Script{keyCount, src, hex.EncodeToString(h.Sum(nil))} 41 | } 42 | 43 | func (s *Script) args(spec string, keysAndArgs []interface{}) []interface{} { 44 | var args []interface{} 45 | if s.keyCount < 0 { 46 | args = make([]interface{}, 1+len(keysAndArgs)) 47 | args[0] = spec 48 | copy(args[1:], keysAndArgs) 49 | } else { 50 | args = make([]interface{}, 2+len(keysAndArgs)) 51 | args[0] = spec 52 | args[1] = s.keyCount 53 | copy(args[2:], keysAndArgs) 54 | } 55 | return args 56 | } 57 | 58 | // Hash returns the script hash. 59 | func (s *Script) Hash() string { 60 | return s.hash 61 | } 62 | 63 | // Do evaluates the script. Under the covers, Do optimistically evaluates the 64 | // script using the EVALSHA command. If the command fails because the script is 65 | // not loaded, then Do evaluates the script using the EVAL command (thus 66 | // causing the script to load). 67 | func (s *Script) Do(c Conn, keysAndArgs ...interface{}) (interface{}, error) { 68 | v, err := c.Do("EVALSHA", s.args(s.hash, keysAndArgs)...) 69 | if e, ok := err.(Error); ok && strings.HasPrefix(string(e), "NOSCRIPT ") { 70 | v, err = c.Do("EVAL", s.args(s.src, keysAndArgs)...) 71 | } 72 | return v, err 73 | } 74 | 75 | // SendHash evaluates the script without waiting for the reply. The script is 76 | // evaluated with the EVALSHA command. The application must ensure that the 77 | // script is loaded by a previous call to Send, Do or Load methods. 78 | func (s *Script) SendHash(c Conn, keysAndArgs ...interface{}) error { 79 | return c.Send("EVALSHA", s.args(s.hash, keysAndArgs)...) 80 | } 81 | 82 | // Send evaluates the script without waiting for the reply. 83 | func (s *Script) Send(c Conn, keysAndArgs ...interface{}) error { 84 | return c.Send("EVAL", s.args(s.src, keysAndArgs)...) 85 | } 86 | 87 | // Load loads the script without evaluating it. 88 | func (s *Script) Load(c Conn) error { 89 | _, err := c.Do("SCRIPT", "LOAD", s.src) 90 | return err 91 | } 92 | -------------------------------------------------------------------------------- /redis/log.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Gary Burd 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package redis 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "log" 21 | ) 22 | 23 | // NewLoggingConn returns a logging wrapper around a connection. 24 | func NewLoggingConn(conn Conn, logger *log.Logger, prefix string) Conn { 25 | if prefix != "" { 26 | prefix = prefix + "." 27 | } 28 | return &loggingConn{conn, logger, prefix} 29 | } 30 | 31 | type loggingConn struct { 32 | Conn 33 | logger *log.Logger 34 | prefix string 35 | } 36 | 37 | func (c *loggingConn) Close() error { 38 | err := c.Conn.Close() 39 | var buf bytes.Buffer 40 | fmt.Fprintf(&buf, "%sClose() -> (%v)", c.prefix, err) 41 | c.logger.Output(2, buf.String()) 42 | return err 43 | } 44 | 45 | func (c *loggingConn) printValue(buf *bytes.Buffer, v interface{}) { 46 | const chop = 32 47 | switch v := v.(type) { 48 | case []byte: 49 | if len(v) > chop { 50 | fmt.Fprintf(buf, "%q...", v[:chop]) 51 | } else { 52 | fmt.Fprintf(buf, "%q", v) 53 | } 54 | case string: 55 | if len(v) > chop { 56 | fmt.Fprintf(buf, "%q...", v[:chop]) 57 | } else { 58 | fmt.Fprintf(buf, "%q", v) 59 | } 60 | case []interface{}: 61 | if len(v) == 0 { 62 | buf.WriteString("[]") 63 | } else { 64 | sep := "[" 65 | fin := "]" 66 | if len(v) > chop { 67 | v = v[:chop] 68 | fin = "...]" 69 | } 70 | for _, vv := range v { 71 | buf.WriteString(sep) 72 | c.printValue(buf, vv) 73 | sep = ", " 74 | } 75 | buf.WriteString(fin) 76 | } 77 | default: 78 | fmt.Fprint(buf, v) 79 | } 80 | } 81 | 82 | func (c *loggingConn) print(method, commandName string, args []interface{}, reply interface{}, err error) { 83 | var buf bytes.Buffer 84 | fmt.Fprintf(&buf, "%s%s(", c.prefix, method) 85 | if method != "Receive" { 86 | buf.WriteString(commandName) 87 | for _, arg := range args { 88 | buf.WriteString(", ") 89 | c.printValue(&buf, arg) 90 | } 91 | } 92 | buf.WriteString(") -> (") 93 | if method != "Send" { 94 | c.printValue(&buf, reply) 95 | buf.WriteString(", ") 96 | } 97 | fmt.Fprintf(&buf, "%v)", err) 98 | c.logger.Output(3, buf.String()) 99 | } 100 | 101 | func (c *loggingConn) Do(commandName string, args ...interface{}) (interface{}, error) { 102 | reply, err := c.Conn.Do(commandName, args...) 103 | c.print("Do", commandName, args, reply, err) 104 | return reply, err 105 | } 106 | 107 | func (c *loggingConn) Send(commandName string, args ...interface{}) error { 108 | err := c.Conn.Send(commandName, args...) 109 | c.print("Send", commandName, args, nil, err) 110 | return err 111 | } 112 | 113 | func (c *loggingConn) Receive() (interface{}, error) { 114 | reply, err := c.Conn.Receive() 115 | c.print("Receive", "", nil, reply, err) 116 | return reply, err 117 | } 118 | -------------------------------------------------------------------------------- /redisx/connmux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Gary Burd 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package redisx 16 | 17 | import ( 18 | "errors" 19 | "sync" 20 | 21 | "github.com/gistao/RedisGo-Async/internal" 22 | "github.com/gistao/RedisGo-Async/redis" 23 | ) 24 | 25 | // ConnMux multiplexes one or more connections to a single underlying 26 | // connection. The ConnMux connections do not support concurrency, commands 27 | // that associate server side state with the connection or commands that put 28 | // the connection in a special mode. 29 | type ConnMux struct { 30 | c redis.Conn 31 | 32 | sendMu sync.Mutex 33 | sendID uint 34 | 35 | recvMu sync.Mutex 36 | recvID uint 37 | recvWait map[uint]chan struct{} 38 | } 39 | 40 | func NewConnMux(c redis.Conn) *ConnMux { 41 | return &ConnMux{c: c, recvWait: make(map[uint]chan struct{})} 42 | } 43 | 44 | // Get gets a connection. The application must close the returned connection. 45 | func (p *ConnMux) Get() redis.Conn { 46 | c := &muxConn{p: p} 47 | c.ids = c.buf[:0] 48 | return c 49 | } 50 | 51 | // Close closes the underlying connection. 52 | func (p *ConnMux) Close() error { 53 | return p.c.Close() 54 | } 55 | 56 | type muxConn struct { 57 | p *ConnMux 58 | ids []uint 59 | buf [8]uint 60 | } 61 | 62 | func (c *muxConn) send(flush bool, cmd string, args ...interface{}) error { 63 | if internal.LookupCommandInfo(cmd).Set != 0 { 64 | return errors.New("command not supported by mux pool") 65 | } 66 | p := c.p 67 | p.sendMu.Lock() 68 | id := p.sendID 69 | c.ids = append(c.ids, id) 70 | p.sendID++ 71 | err := p.c.Send(cmd, args...) 72 | if flush { 73 | err = p.c.Flush() 74 | } 75 | p.sendMu.Unlock() 76 | return err 77 | } 78 | 79 | func (c *muxConn) Send(cmd string, args ...interface{}) error { 80 | return c.send(false, cmd, args...) 81 | } 82 | 83 | func (c *muxConn) Flush() error { 84 | p := c.p 85 | p.sendMu.Lock() 86 | err := p.c.Flush() 87 | p.sendMu.Unlock() 88 | return err 89 | } 90 | 91 | func (c *muxConn) Receive() (interface{}, error) { 92 | if len(c.ids) == 0 { 93 | return nil, errors.New("mux pool underflow") 94 | } 95 | 96 | id := c.ids[0] 97 | c.ids = c.ids[1:] 98 | if len(c.ids) == 0 { 99 | c.ids = c.buf[:0] 100 | } 101 | 102 | p := c.p 103 | p.recvMu.Lock() 104 | if p.recvID != id { 105 | ch := make(chan struct{}) 106 | p.recvWait[id] = ch 107 | p.recvMu.Unlock() 108 | <-ch 109 | p.recvMu.Lock() 110 | if p.recvID != id { 111 | panic("out of sync") 112 | } 113 | } 114 | 115 | v, err := p.c.Receive() 116 | 117 | id++ 118 | p.recvID = id 119 | ch, ok := p.recvWait[id] 120 | if ok { 121 | delete(p.recvWait, id) 122 | } 123 | p.recvMu.Unlock() 124 | if ok { 125 | ch <- struct{}{} 126 | } 127 | 128 | return v, err 129 | } 130 | 131 | func (c *muxConn) Close() error { 132 | var err error 133 | if len(c.ids) == 0 { 134 | return nil 135 | } 136 | c.Flush() 137 | for _ = range c.ids { 138 | _, err = c.Receive() 139 | } 140 | return err 141 | } 142 | 143 | func (c *muxConn) Do(cmd string, args ...interface{}) (interface{}, error) { 144 | if err := c.send(true, cmd, args...); err != nil { 145 | return nil, err 146 | } 147 | return c.Receive() 148 | } 149 | 150 | func (c *muxConn) Err() error { 151 | return c.p.c.Err() 152 | } 153 | -------------------------------------------------------------------------------- /redis/pubsub.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Gary Burd 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package redis 16 | 17 | import "errors" 18 | 19 | // Subscription represents a subscribe or unsubscribe notification. 20 | type Subscription struct { 21 | 22 | // Kind is "subscribe", "unsubscribe", "psubscribe" or "punsubscribe" 23 | Kind string 24 | 25 | // The channel that was changed. 26 | Channel string 27 | 28 | // The current number of subscriptions for connection. 29 | Count int 30 | } 31 | 32 | // Message represents a message notification. 33 | type Message struct { 34 | 35 | // The originating channel. 36 | Channel string 37 | 38 | // The message data. 39 | Data []byte 40 | } 41 | 42 | // PMessage represents a pmessage notification. 43 | type PMessage struct { 44 | 45 | // The matched pattern. 46 | Pattern string 47 | 48 | // The originating channel. 49 | Channel string 50 | 51 | // The message data. 52 | Data []byte 53 | } 54 | 55 | // Pong represents a pubsub pong notification. 56 | type Pong struct { 57 | Data string 58 | } 59 | 60 | // PubSubConn wraps a Conn with convenience methods for subscribers. 61 | type PubSubConn struct { 62 | Conn Conn 63 | } 64 | 65 | // Close closes the connection. 66 | func (c PubSubConn) Close() error { 67 | return c.Conn.Close() 68 | } 69 | 70 | // Subscribe subscribes the connection to the specified channels. 71 | func (c PubSubConn) Subscribe(channel ...interface{}) error { 72 | c.Conn.Send("SUBSCRIBE", channel...) 73 | return c.Conn.Flush() 74 | } 75 | 76 | // PSubscribe subscribes the connection to the given patterns. 77 | func (c PubSubConn) PSubscribe(channel ...interface{}) error { 78 | c.Conn.Send("PSUBSCRIBE", channel...) 79 | return c.Conn.Flush() 80 | } 81 | 82 | // Unsubscribe unsubscribes the connection from the given channels, or from all 83 | // of them if none is given. 84 | func (c PubSubConn) Unsubscribe(channel ...interface{}) error { 85 | c.Conn.Send("UNSUBSCRIBE", channel...) 86 | return c.Conn.Flush() 87 | } 88 | 89 | // PUnsubscribe unsubscribes the connection from the given patterns, or from all 90 | // of them if none is given. 91 | func (c PubSubConn) PUnsubscribe(channel ...interface{}) error { 92 | c.Conn.Send("PUNSUBSCRIBE", channel...) 93 | return c.Conn.Flush() 94 | } 95 | 96 | // Ping sends a PING to the server with the specified data. 97 | func (c PubSubConn) Ping(data string) error { 98 | c.Conn.Send("PING", data) 99 | return c.Conn.Flush() 100 | } 101 | 102 | // Receive returns a pushed message as a Subscription, Message, PMessage, Pong 103 | // or error. The return value is intended to be used directly in a type switch 104 | // as illustrated in the PubSubConn example. 105 | func (c PubSubConn) Receive() interface{} { 106 | reply, err := Values(c.Conn.Receive()) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | var kind string 112 | reply, err = Scan(reply, &kind) 113 | if err != nil { 114 | return err 115 | } 116 | 117 | switch kind { 118 | case "message": 119 | var m Message 120 | if _, err := Scan(reply, &m.Channel, &m.Data); err != nil { 121 | return err 122 | } 123 | return m 124 | case "pmessage": 125 | var pm PMessage 126 | if _, err := Scan(reply, &pm.Pattern, &pm.Channel, &pm.Data); err != nil { 127 | return err 128 | } 129 | return pm 130 | case "subscribe", "psubscribe", "unsubscribe", "punsubscribe": 131 | s := Subscription{Kind: kind} 132 | if _, err := Scan(reply, &s.Channel, &s.Count); err != nil { 133 | return err 134 | } 135 | return s 136 | case "pong": 137 | var p Pong 138 | if _, err := Scan(reply, &p.Data); err != nil { 139 | return err 140 | } 141 | return p 142 | } 143 | return errors.New("RedisGo-Async: unknown pubsub notification") 144 | } 145 | -------------------------------------------------------------------------------- /bench/asyncbench/asyncbench.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 xiaofei, gistao 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "errors" 19 | "flag" 20 | "fmt" 21 | "log" 22 | "runtime" 23 | "time" 24 | ) 25 | 26 | // redis task function type def 27 | type redisTask func(id string, signal chan int, cli *AsynClient, iterations int) 28 | 29 | // task info 30 | type taskSpec struct { 31 | task redisTask 32 | name string 33 | } 34 | 35 | // array of Tasks to run in sequence 36 | // Add a task to the list to bench to the runner. 37 | // Tasks are run in sequence. 38 | var tasks = []taskSpec{ 39 | taskSpec{doSet, "SET"}, 40 | taskSpec{doGet, "GET"}, 41 | taskSpec{doDel, "DEL"}, 42 | } 43 | 44 | // workers option. default is equiv to -w=10000 on command line 45 | var workers = flag.Int("w", 10000, "number of concurrent workers") 46 | 47 | // opcnt option. default is equiv to -n=20 on command line 48 | var opcnt = flag.Int("n", 1000, "number of task iterations per worker") 49 | 50 | // ---------------------------------------------------------------------------- 51 | // benchmarker 52 | // ---------------------------------------------------------------------------- 53 | 54 | func main() { 55 | // DEBUG 56 | runtime.GOMAXPROCS(2) 57 | 58 | log.SetPrefix("[RedisGo-Async|async bench] ") 59 | flag.Parse() 60 | 61 | fmt.Printf("\n\n== Bench RedisGo-Async == %d goroutines 1 AsyncClient -- %d opts each --- \n\n", *workers, *opcnt) 62 | 63 | for _, task := range tasks { 64 | benchTask(task, *opcnt, *workers, true) 65 | } 66 | 67 | } 68 | 69 | func benchTask(taskspec taskSpec, iterations int, workers int, printReport bool) (delta time.Duration, err error) { 70 | 71 | signal := make(chan int, workers) 72 | 73 | rdc := GetAsynClient("127.0.0.1:6379") 74 | if rdc == nil { 75 | log.Println("Error creating client for worker") 76 | return -1, errors.New("Error creating client for worker") 77 | } 78 | 79 | t0 := time.Now() 80 | for i := 0; i < workers; i++ { 81 | id := fmt.Sprintf("%d", i) 82 | go taskspec.task(id, signal, rdc, iterations) 83 | } 84 | 85 | // wait for completion 86 | for i := 0; i < workers; i++ { 87 | <-signal 88 | } 89 | delta = time.Since(t0) 90 | 91 | if printReport { 92 | report(taskspec.name, workers, delta, iterations*workers) 93 | } 94 | 95 | return 96 | } 97 | 98 | func report(cmd string, workers int, delta time.Duration, cnt int) { 99 | log.Printf("---\n") 100 | log.Printf("cmd: %s\n", cmd) 101 | log.Printf("%d goroutines 1 asyncClient %d iterations of %s in %d msecs\n", workers, cnt, cmd, delta/time.Millisecond) 102 | log.Printf("---\n\n") 103 | } 104 | 105 | // ---------------------------------------------------------------------------- 106 | // redis tasks 107 | // ---------------------------------------------------------------------------- 108 | 109 | func doSet(id string, signal chan int, cli *AsynClient, cnt int) { 110 | key := "set-" + id 111 | value := "foo" 112 | for i := 0; i < cnt; i++ { 113 | //log.Println("set", key, value) 114 | _, err := cli.Set(key, value) 115 | //log.Println("set over", key, value) 116 | if err != nil { 117 | log.Println("set", err) 118 | } 119 | } 120 | signal <- 1 121 | } 122 | 123 | func doGet(id string, signal chan int, cli *AsynClient, cnt int) { 124 | key := "set-" + id 125 | for i := 0; i < cnt; i++ { 126 | // log.Println("get", key) 127 | _, err := cli.Get(key) 128 | if err != nil { 129 | log.Println(err) 130 | } 131 | } 132 | signal <- 1 133 | } 134 | 135 | func doDel(id string, signal chan int, cli *AsynClient, cnt int) { 136 | key := "set-" + id 137 | for i := 0; i < cnt; i++ { 138 | _, err := cli.Del(key) 139 | if err != nil { 140 | log.Println(err) 141 | } 142 | } 143 | signal <- 1 144 | } 145 | -------------------------------------------------------------------------------- /bench/syncbench/syncbench.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 xiaofei, gistao 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "errors" 19 | "flag" 20 | "fmt" 21 | "log" 22 | "runtime" 23 | "time" 24 | ) 25 | 26 | // redis task function type def 27 | type redisTask func(id string, signal chan int, cli *SynClient, iterations int) 28 | 29 | // task info 30 | type taskSpec struct { 31 | task redisTask 32 | name string 33 | } 34 | 35 | // array of Tasks to run in sequence 36 | // Add a task to the list to bench to the runner. 37 | // Tasks are run in sequence. 38 | var tasks = []taskSpec{ 39 | taskSpec{doSet, "SET"}, 40 | taskSpec{doGet, "GET"}, 41 | taskSpec{doDel, "DEL"}, 42 | } 43 | 44 | // workers option. default is equiv to -w=10000 on command line 45 | var workers = flag.Int("w", 10000, "number of concurrent workers") 46 | 47 | // opcnt option. default is equiv to -n=20 on command line 48 | var opcnt = flag.Int("n", 1000, "number of task iterations per worker") 49 | 50 | // ---------------------------------------------------------------------------- 51 | // benchmarker 52 | // ---------------------------------------------------------------------------- 53 | 54 | func main() { 55 | // DEBUG 56 | runtime.GOMAXPROCS(2) 57 | 58 | log.SetPrefix("[RedisGo-Async|sync bench] ") 59 | flag.Parse() 60 | 61 | fmt.Printf("\n\n== Bench RedisGo-Async == %d goroutines max 100 SyncClient -- %d opts each --- \n\n", *workers, *opcnt) 62 | 63 | for _, task := range tasks { 64 | benchTask(task, *opcnt, *workers, true) 65 | } 66 | 67 | } 68 | 69 | func benchTask(taskspec taskSpec, iterations int, workers int, printReport bool) (delta time.Duration, err error) { 70 | 71 | signal := make(chan int, workers) 72 | 73 | rdc := GetSynClient("127.0.0.1:6379") 74 | if rdc == nil { 75 | log.Println("Error creating client for worker") 76 | return -1, errors.New("Error creating client for worker") 77 | } 78 | 79 | t0 := time.Now() 80 | for i := 0; i < workers; i++ { 81 | id := fmt.Sprintf("%d", i) 82 | go taskspec.task(id, signal, rdc, iterations) 83 | } 84 | 85 | // wait for completion 86 | for i := 0; i < workers; i++ { 87 | <-signal 88 | } 89 | delta = time.Since(t0) 90 | 91 | if printReport { 92 | report(taskspec.name, workers, delta, iterations*workers) 93 | } 94 | 95 | return 96 | } 97 | 98 | func report(cmd string, workers int, delta time.Duration, cnt int) { 99 | log.Printf("---\n") 100 | log.Printf("cmd: %s\n", cmd) 101 | log.Printf("%d goroutines max 100 syncClient %d iterations of %s in %d msecs\n", workers, cnt, cmd, delta/time.Millisecond) 102 | log.Printf("---\n\n") 103 | } 104 | 105 | // ---------------------------------------------------------------------------- 106 | // redis tasks 107 | // ---------------------------------------------------------------------------- 108 | 109 | func doSet(id string, signal chan int, cli *SynClient, cnt int) { 110 | key := "set-" + id 111 | value := "foo" 112 | for i := 0; i < cnt; i++ { 113 | //log.Println("set", key, value) 114 | _, err := cli.Set(key, value) 115 | //log.Println("set over", key, value) 116 | if err != nil { 117 | log.Println("set", err) 118 | } 119 | } 120 | signal <- 1 121 | } 122 | 123 | func doGet(id string, signal chan int, cli *SynClient, cnt int) { 124 | key := "set-" + id 125 | for i := 0; i < cnt; i++ { 126 | // log.Println("get", key) 127 | _, err := cli.Get(key) 128 | if err != nil { 129 | log.Println(err) 130 | } 131 | } 132 | signal <- 1 133 | } 134 | 135 | func doDel(id string, signal chan int, cli *SynClient, cnt int) { 136 | key := "set-" + id 137 | for i := 0; i < cnt; i++ { 138 | _, err := cli.Del(key) 139 | if err != nil { 140 | log.Println(err) 141 | } 142 | } 143 | signal <- 1 144 | } 145 | -------------------------------------------------------------------------------- /redis/pubsub_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Gary Burd 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package redis_test 16 | 17 | import ( 18 | "fmt" 19 | "reflect" 20 | "sync" 21 | "testing" 22 | 23 | "redis" 24 | ) 25 | 26 | func publish(channel, value interface{}) { 27 | c, err := dial() 28 | if err != nil { 29 | fmt.Println(err) 30 | return 31 | } 32 | defer c.Close() 33 | c.Do("PUBLISH", channel, value) 34 | } 35 | 36 | // Applications can receive pushed messages from one goroutine and manage subscriptions from another goroutine. 37 | func ExamplePubSubConn() { 38 | c, err := dial() 39 | if err != nil { 40 | fmt.Println(err) 41 | return 42 | } 43 | defer c.Close() 44 | var wg sync.WaitGroup 45 | wg.Add(2) 46 | 47 | psc := redis.PubSubConn{Conn: c} 48 | 49 | // This goroutine receives and prints pushed notifications from the server. 50 | // The goroutine exits when the connection is unsubscribed from all 51 | // channels or there is an error. 52 | go func() { 53 | defer wg.Done() 54 | for { 55 | switch n := psc.Receive().(type) { 56 | case redis.Message: 57 | fmt.Printf("Message: %s %s\n", n.Channel, n.Data) 58 | case redis.PMessage: 59 | fmt.Printf("PMessage: %s %s %s\n", n.Pattern, n.Channel, n.Data) 60 | case redis.Subscription: 61 | fmt.Printf("Subscription: %s %s %d\n", n.Kind, n.Channel, n.Count) 62 | if n.Count == 0 { 63 | return 64 | } 65 | case error: 66 | fmt.Printf("error: %v\n", n) 67 | return 68 | } 69 | } 70 | }() 71 | 72 | // This goroutine manages subscriptions for the connection. 73 | go func() { 74 | defer wg.Done() 75 | 76 | psc.Subscribe("example") 77 | psc.PSubscribe("p*") 78 | 79 | // The following function calls publish a message using another 80 | // connection to the Redis server. 81 | publish("example", "hello") 82 | publish("example", "world") 83 | publish("pexample", "foo") 84 | publish("pexample", "bar") 85 | 86 | // Unsubscribe from all connections. This will cause the receiving 87 | // goroutine to exit. 88 | psc.Unsubscribe() 89 | psc.PUnsubscribe() 90 | }() 91 | 92 | wg.Wait() 93 | 94 | // Output: 95 | // Subscription: subscribe example 1 96 | // Subscription: psubscribe p* 2 97 | // Message: example hello 98 | // Message: example world 99 | // PMessage: p* pexample foo 100 | // PMessage: p* pexample bar 101 | // Subscription: unsubscribe example 1 102 | // Subscription: punsubscribe p* 0 103 | } 104 | 105 | func expectPushed(t *testing.T, c redis.PubSubConn, message string, expected interface{}) { 106 | actual := c.Receive() 107 | if !reflect.DeepEqual(actual, expected) { 108 | t.Errorf("%s = %v, want %v", message, actual, expected) 109 | } 110 | } 111 | 112 | func TestPushed(t *testing.T) { 113 | pc, err := redis.DialDefaultServer() 114 | if err != nil { 115 | t.Fatalf("error connection to database, %v", err) 116 | } 117 | defer pc.Close() 118 | 119 | sc, err := redis.DialDefaultServer() 120 | if err != nil { 121 | t.Fatalf("error connection to database, %v", err) 122 | } 123 | defer sc.Close() 124 | 125 | c := redis.PubSubConn{Conn: sc} 126 | 127 | c.Subscribe("c1") 128 | expectPushed(t, c, "Subscribe(c1)", redis.Subscription{Kind: "subscribe", Channel: "c1", Count: 1}) 129 | c.Subscribe("c2") 130 | expectPushed(t, c, "Subscribe(c2)", redis.Subscription{Kind: "subscribe", Channel: "c2", Count: 2}) 131 | c.PSubscribe("p1") 132 | expectPushed(t, c, "PSubscribe(p1)", redis.Subscription{Kind: "psubscribe", Channel: "p1", Count: 3}) 133 | c.PSubscribe("p2") 134 | expectPushed(t, c, "PSubscribe(p2)", redis.Subscription{Kind: "psubscribe", Channel: "p2", Count: 4}) 135 | c.PUnsubscribe() 136 | expectPushed(t, c, "Punsubscribe(p1)", redis.Subscription{Kind: "punsubscribe", Channel: "p1", Count: 3}) 137 | expectPushed(t, c, "Punsubscribe()", redis.Subscription{Kind: "punsubscribe", Channel: "p2", Count: 2}) 138 | 139 | pc.Do("PUBLISH", "c1", "hello") 140 | expectPushed(t, c, "PUBLISH c1 hello", redis.Message{Channel: "c1", Data: []byte("hello")}) 141 | 142 | c.Ping("hello") 143 | expectPushed(t, c, `Ping("hello")`, redis.Pong{Data: "hello"}) 144 | 145 | c.Conn.Send("PING") 146 | c.Conn.Flush() 147 | expectPushed(t, c, `Send("PING")`, redis.Pong{}) 148 | } 149 | -------------------------------------------------------------------------------- /redis/reply_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Gary Burd 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package redis_test 16 | 17 | import ( 18 | "fmt" 19 | "reflect" 20 | "testing" 21 | 22 | "github.com/gistao/RedisGo-Async/redis" 23 | ) 24 | 25 | type valueError struct { 26 | v interface{} 27 | err error 28 | } 29 | 30 | func ve(v interface{}, err error) valueError { 31 | return valueError{v, err} 32 | } 33 | 34 | var replyTests = []struct { 35 | name interface{} 36 | actual valueError 37 | expected valueError 38 | }{ 39 | { 40 | "ints([v1, v2])", 41 | ve(redis.Ints([]interface{}{[]byte("4"), []byte("5")}, nil)), 42 | ve([]int{4, 5}, nil), 43 | }, 44 | { 45 | "ints(nil)", 46 | ve(redis.Ints(nil, nil)), 47 | ve([]int(nil), redis.ErrNil), 48 | }, 49 | { 50 | "strings([v1, v2])", 51 | ve(redis.Strings([]interface{}{[]byte("v1"), []byte("v2")}, nil)), 52 | ve([]string{"v1", "v2"}, nil), 53 | }, 54 | { 55 | "strings(nil)", 56 | ve(redis.Strings(nil, nil)), 57 | ve([]string(nil), redis.ErrNil), 58 | }, 59 | { 60 | "byteslices([v1, v2])", 61 | ve(redis.ByteSlices([]interface{}{[]byte("v1"), []byte("v2")}, nil)), 62 | ve([][]byte{[]byte("v1"), []byte("v2")}, nil), 63 | }, 64 | { 65 | "byteslices(nil)", 66 | ve(redis.ByteSlices(nil, nil)), 67 | ve([][]byte(nil), redis.ErrNil), 68 | }, 69 | { 70 | "values([v1, v2])", 71 | ve(redis.Values([]interface{}{[]byte("v1"), []byte("v2")}, nil)), 72 | ve([]interface{}{[]byte("v1"), []byte("v2")}, nil), 73 | }, 74 | { 75 | "values(nil)", 76 | ve(redis.Values(nil, nil)), 77 | ve([]interface{}(nil), redis.ErrNil), 78 | }, 79 | { 80 | "float64(1.0)", 81 | ve(redis.Float64([]byte("1.0"), nil)), 82 | ve(float64(1.0), nil), 83 | }, 84 | { 85 | "float64(nil)", 86 | ve(redis.Float64(nil, nil)), 87 | ve(float64(0.0), redis.ErrNil), 88 | }, 89 | { 90 | "uint64(1)", 91 | ve(redis.Uint64(int64(1), nil)), 92 | ve(uint64(1), nil), 93 | }, 94 | { 95 | "uint64(-1)", 96 | ve(redis.Uint64(int64(-1), nil)), 97 | ve(uint64(0), redis.ErrNegativeInt), 98 | }, 99 | { 100 | "positions([[1, 2], nil, [3, 4]])", 101 | ve(redis.Positions([]interface{}{[]interface{}{[]byte("1"), []byte("2")}, nil, []interface{}{[]byte("3"), []byte("4")}}, nil)), 102 | ve([]*[2]float64{{1.0, 2.0}, nil, {3.0, 4.0}}, nil), 103 | }, 104 | } 105 | 106 | func TestReply(t *testing.T) { 107 | for _, rt := range replyTests { 108 | if rt.actual.err != rt.expected.err { 109 | t.Errorf("%s returned err %v, want %v", rt.name, rt.actual.err, rt.expected.err) 110 | continue 111 | } 112 | if !reflect.DeepEqual(rt.actual.v, rt.expected.v) { 113 | t.Errorf("%s=%+v, want %+v", rt.name, rt.actual.v, rt.expected.v) 114 | } 115 | } 116 | } 117 | 118 | // dial wraps DialDefaultServer() with a more suitable function name for examples. 119 | func dial() (redis.Conn, error) { 120 | return redis.DialDefaultServer() 121 | } 122 | 123 | func ExampleBool() { 124 | c, err := dial() 125 | if err != nil { 126 | fmt.Println(err) 127 | return 128 | } 129 | defer c.Close() 130 | 131 | c.Do("SET", "foo", 1) 132 | exists, _ := redis.Bool(c.Do("EXISTS", "foo")) 133 | fmt.Printf("%#v\n", exists) 134 | // Output: 135 | // true 136 | } 137 | 138 | func ExampleInt() { 139 | c, err := dial() 140 | if err != nil { 141 | fmt.Println(err) 142 | return 143 | } 144 | defer c.Close() 145 | 146 | c.Do("SET", "k1", 1) 147 | n, _ := redis.Int(c.Do("GET", "k1")) 148 | fmt.Printf("%#v\n", n) 149 | n, _ = redis.Int(c.Do("INCR", "k1")) 150 | fmt.Printf("%#v\n", n) 151 | // Output: 152 | // 1 153 | // 2 154 | } 155 | 156 | func ExampleInts() { 157 | c, err := dial() 158 | if err != nil { 159 | fmt.Println(err) 160 | return 161 | } 162 | defer c.Close() 163 | 164 | c.Do("SADD", "set_with_integers", 4, 5, 6) 165 | ints, _ := redis.Ints(c.Do("SMEMBERS", "set_with_integers")) 166 | fmt.Printf("%#v\n", ints) 167 | // Output: 168 | // []int{4, 5, 6} 169 | } 170 | 171 | func ExampleString() { 172 | c, err := dial() 173 | if err != nil { 174 | fmt.Println(err) 175 | return 176 | } 177 | defer c.Close() 178 | 179 | c.Do("SET", "hello", "world") 180 | s, err := redis.String(c.Do("GET", "hello")) 181 | fmt.Printf("%#v\n", s) 182 | // Output: 183 | // "world" 184 | } 185 | -------------------------------------------------------------------------------- /redis/test_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Gary Burd 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package redis 16 | 17 | import ( 18 | "bufio" 19 | "errors" 20 | "flag" 21 | "fmt" 22 | "io" 23 | "io/ioutil" 24 | "os" 25 | "os/exec" 26 | "strconv" 27 | "strings" 28 | "sync" 29 | "testing" 30 | "time" 31 | ) 32 | 33 | func SetNowFunc(f func() time.Time) { 34 | nowFunc = f 35 | } 36 | 37 | var ( 38 | ErrNegativeInt = errNegativeInt 39 | 40 | serverPath = flag.String("redis-server", "redis-server", "Path to redis server binary") 41 | serverBasePort = flag.Int("redis-port", 16379, "Beginning of port range for test servers") 42 | serverLogName = flag.String("redis-log", "", "Write Redis server logs to `filename`") 43 | serverLog = ioutil.Discard 44 | 45 | defaultServerMu sync.Mutex 46 | defaultServer *Server 47 | defaultServerErr error 48 | ) 49 | 50 | type Server struct { 51 | name string 52 | cmd *exec.Cmd 53 | done chan struct{} 54 | } 55 | 56 | func NewServer(name string, args ...string) (*Server, error) { 57 | s := &Server{ 58 | name: name, 59 | cmd: exec.Command(*serverPath, args...), 60 | done: make(chan struct{}), 61 | } 62 | 63 | r, err := s.cmd.StdoutPipe() 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | err = s.cmd.Start() 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | ready := make(chan error, 1) 74 | go s.watch(r, ready) 75 | 76 | select { 77 | case err = <-ready: 78 | case <-time.After(time.Second * 10): 79 | err = errors.New("timeout waiting for server to start") 80 | } 81 | 82 | if err != nil { 83 | s.Stop() 84 | return nil, err 85 | } 86 | 87 | return s, nil 88 | } 89 | 90 | func (s *Server) watch(r io.Reader, ready chan error) { 91 | fmt.Fprintf(serverLog, "%d START %s \n", s.cmd.Process.Pid, s.name) 92 | var listening bool 93 | var text string 94 | scn := bufio.NewScanner(r) 95 | for scn.Scan() { 96 | text = scn.Text() 97 | fmt.Fprintf(serverLog, "%s\n", text) 98 | if !listening { 99 | if strings.Contains(text, "The server is now ready to accept connections on port") { 100 | listening = true 101 | ready <- nil 102 | } 103 | } 104 | } 105 | if !listening { 106 | ready <- fmt.Errorf("server exited: %s", text) 107 | } 108 | s.cmd.Wait() 109 | fmt.Fprintf(serverLog, "%d STOP %s \n", s.cmd.Process.Pid, s.name) 110 | close(s.done) 111 | } 112 | 113 | func (s *Server) Stop() { 114 | s.cmd.Process.Signal(os.Interrupt) 115 | <-s.done 116 | } 117 | 118 | // stopDefaultServer stops the server created by DialDefaultServer. 119 | func stopDefaultServer() { 120 | defaultServerMu.Lock() 121 | defer defaultServerMu.Unlock() 122 | if defaultServer != nil { 123 | defaultServer.Stop() 124 | defaultServer = nil 125 | } 126 | } 127 | 128 | // startDefaultServer starts the default server if not already running. 129 | func startDefaultServer() error { 130 | defaultServerMu.Lock() 131 | defer defaultServerMu.Unlock() 132 | if defaultServer != nil || defaultServerErr != nil { 133 | return defaultServerErr 134 | } 135 | defaultServer, defaultServerErr = NewServer( 136 | "default", 137 | "--port", strconv.Itoa(*serverBasePort), 138 | "--save", "", 139 | "--appendonly", "no") 140 | return defaultServerErr 141 | } 142 | 143 | // DialDefaultServer starts the test server if not already started and dials a 144 | // connection to the server. 145 | func DialDefaultServer() (Conn, error) { 146 | if err := startDefaultServer(); err != nil { 147 | return nil, err 148 | } 149 | c, err := Dial("tcp", fmt.Sprintf(":%d", *serverBasePort), DialReadTimeout(1*time.Second), DialWriteTimeout(1*time.Second)) 150 | if err != nil { 151 | return nil, err 152 | } 153 | c.Do("FLUSHDB") 154 | return c, nil 155 | } 156 | 157 | func TestMain(m *testing.M) { 158 | os.Exit(func() int { 159 | flag.Parse() 160 | 161 | var f *os.File 162 | if *serverLogName != "" { 163 | var err error 164 | f, err = os.OpenFile(*serverLogName, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600) 165 | if err != nil { 166 | fmt.Fprintf(os.Stderr, "Error opening redis-log: %v\n", err) 167 | return 1 168 | } 169 | defer f.Close() 170 | serverLog = f 171 | } 172 | 173 | defer stopDefaultServer() 174 | 175 | return m.Run() 176 | }()) 177 | } 178 | -------------------------------------------------------------------------------- /redis/asyncpool.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Gary Burd 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package redis 16 | 17 | import ( 18 | "errors" 19 | "sync" 20 | "sync/atomic" 21 | "time" 22 | ) 23 | 24 | var errorCompatibility = errors.New("RedisGo-Async: should use AsyncDo func") 25 | 26 | // AsyncPool maintains one connection. 27 | type AsyncPool struct { 28 | // Dial is an application supplied function for creating and configuring a 29 | // connection. 30 | // 31 | // The connection returned from Dial must not be in a special state 32 | // (subscribed to pubsub channel, transaction started, ...). 33 | Dial func() (AsynConn, error) 34 | // TestOnBorrow is an optional application supplied function for checking 35 | // the health of an idle connection before the connection is used again by 36 | // the application. Argument t is the time that the connection was returned 37 | // to the pool. If the function returns an error, then the connection is 38 | // closed. 39 | TestOnBorrow func(c AsynConn, t time.Time) error 40 | // MaxGetCount is the maximum value that limits the hang up 'Get()' goroutine. 41 | // When zero, there is no limit. 42 | MaxGetCount int 43 | // MaxGetCount is the maximum value that limits the hang up 'Do()' goroutine. 44 | // When zero, there is no limit. 45 | MaxDoCount int 46 | 47 | c *asyncPoolConnection 48 | mu sync.Mutex 49 | cond *sync.Cond 50 | getCount int 51 | doCount int32 52 | closed bool 53 | blocking bool 54 | } 55 | 56 | // NewAsyncPool creates a new async pool. 57 | func NewAsyncPool(newFn func() (AsynConn, error), testFn func(AsynConn, time.Time) error) *AsyncPool { 58 | return &AsyncPool{Dial: newFn, TestOnBorrow: testFn} 59 | } 60 | 61 | // Get gets a connection. 62 | func (p *AsyncPool) Get() AsynConn { 63 | p.mu.Lock() 64 | if p.cond == nil { 65 | p.cond = sync.NewCond(&p.mu) 66 | } 67 | 68 | p.getCount++ 69 | if p.MaxGetCount != 0 && p.getCount > p.MaxGetCount { 70 | p.getCount-- 71 | p.mu.Unlock() 72 | return errorConnection{ErrPoolExhausted} 73 | } 74 | 75 | var pc AsynConn 76 | for { 77 | if p.closed { 78 | p.getCount-- 79 | p.mu.Unlock() 80 | return errorConnection{errPoolClosed} 81 | } 82 | 83 | if p.blocking { 84 | p.cond.Wait() 85 | continue 86 | } 87 | 88 | if p.c != nil && p.c.Err() == nil { 89 | if test := p.TestOnBorrow; test != nil { 90 | p.blocking = true 91 | ic := p.c.c.(*asynConn) 92 | p.mu.Unlock() 93 | 94 | err := test(p.c, ic.t) 95 | 96 | p.mu.Lock() 97 | p.blocking = false 98 | if err == nil { 99 | pc = p.c 100 | p.getCount-- 101 | p.cond.Signal() 102 | p.mu.Unlock() 103 | return pc 104 | } 105 | } else { 106 | pc = p.c 107 | p.getCount-- 108 | p.cond.Signal() 109 | p.mu.Unlock() 110 | return pc 111 | } 112 | } 113 | 114 | if p.c != nil { 115 | p.c.c.Close() 116 | } 117 | p.blocking = true 118 | p.mu.Unlock() 119 | 120 | c, err := p.Dial() 121 | 122 | p.mu.Lock() 123 | p.blocking = false 124 | if err != nil { 125 | p.getCount-- 126 | p.cond.Signal() 127 | p.mu.Unlock() 128 | return errorConnection{err} 129 | } 130 | 131 | p.c = &asyncPoolConnection{p: p, c: c} 132 | pc := p.c 133 | p.getCount-- 134 | p.cond.Signal() 135 | p.mu.Unlock() 136 | 137 | return pc 138 | } 139 | } 140 | 141 | // ActiveCount returns the number of client of this pool. 142 | func (p *AsyncPool) ActiveCount() int { 143 | p.mu.Lock() 144 | defer p.mu.Unlock() 145 | 146 | if p.c != nil && p.c.Err() == nil { 147 | return 1 148 | } 149 | return 0 150 | } 151 | 152 | // IdleCount returns the number of idle connections in the pool. 153 | func (p *AsyncPool) IdleCount() int { 154 | return 0 155 | } 156 | 157 | // Close releases the resources used by the pool. 158 | func (p *AsyncPool) Close() error { 159 | p.mu.Lock() 160 | defer p.mu.Unlock() 161 | 162 | if p.closed { 163 | return nil 164 | } 165 | p.closed = true 166 | if p.cond != nil { 167 | p.cond.Broadcast() 168 | } 169 | err := p.c.c.Close() 170 | p.c = nil 171 | 172 | return err 173 | } 174 | 175 | type asyncPoolConnection struct { 176 | p *AsyncPool 177 | c AsynConn 178 | } 179 | 180 | func (pc *asyncPoolConnection) Close() error { 181 | return nil 182 | } 183 | 184 | func (pc *asyncPoolConnection) Err() error { 185 | return pc.c.Err() 186 | } 187 | 188 | func (pc *asyncPoolConnection) Do(commandName string, args ...interface{}) (reply interface{}, err error) { 189 | if pc.p.MaxDoCount != 0 { 190 | if atomic.AddInt32(&pc.p.doCount, 1) > int32(pc.p.MaxDoCount) { 191 | atomic.AddInt32(&pc.p.doCount, -1) 192 | return nil, ErrPoolExhausted 193 | } 194 | 195 | defer func() { 196 | atomic.AddInt32(&pc.p.doCount, -1) 197 | }() 198 | } 199 | 200 | return pc.c.Do(commandName, args...) 201 | } 202 | 203 | func (pc *asyncPoolConnection) AsyncDo(commandName string, args ...interface{}) (ret AsyncRet, err error) { 204 | return pc.c.AsyncDo(commandName, args...) 205 | } 206 | 207 | func (pc *asyncPoolConnection) Send(commandName string, args ...interface{}) error { 208 | return errorCompatibility 209 | } 210 | 211 | func (pc *asyncPoolConnection) Flush() error { 212 | return errorCompatibility 213 | } 214 | 215 | func (pc *asyncPoolConnection) Receive() (reply interface{}, err error) { 216 | return nil, errorCompatibility 217 | } 218 | 219 | func (ec errorConnection) AsyncDo(string, ...interface{}) (AsyncRet, error) { return nil, ec.err } 220 | -------------------------------------------------------------------------------- /redisx/connmux_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Gary Burd 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package redisx_test 16 | 17 | import ( 18 | "net/textproto" 19 | "sync" 20 | "testing" 21 | 22 | "github.com/gistao/RedisGo-Async/internal/redistest" 23 | "github.com/gistao/RedisGo-Async/redis" 24 | "github.com/gistao/RedisGo-Async/redisx" 25 | ) 26 | 27 | func TestConnMux(t *testing.T) { 28 | c, err := redistest.Dial() 29 | if err != nil { 30 | t.Fatalf("error connection to database, %v", err) 31 | } 32 | m := redisx.NewConnMux(c) 33 | defer m.Close() 34 | 35 | c1 := m.Get() 36 | c2 := m.Get() 37 | c1.Send("ECHO", "hello") 38 | c2.Send("ECHO", "world") 39 | c1.Flush() 40 | c2.Flush() 41 | s, err := redis.String(c1.Receive()) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | if s != "hello" { 46 | t.Fatalf("echo returned %q, want %q", s, "hello") 47 | } 48 | s, err = redis.String(c2.Receive()) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | if s != "world" { 53 | t.Fatalf("echo returned %q, want %q", s, "world") 54 | } 55 | c1.Close() 56 | c2.Close() 57 | } 58 | 59 | func TestConnMuxClose(t *testing.T) { 60 | c, err := redistest.Dial() 61 | if err != nil { 62 | t.Fatalf("error connection to database, %v", err) 63 | } 64 | m := redisx.NewConnMux(c) 65 | defer m.Close() 66 | 67 | c1 := m.Get() 68 | c2 := m.Get() 69 | 70 | if err := c1.Send("ECHO", "hello"); err != nil { 71 | t.Fatal(err) 72 | } 73 | if err := c1.Close(); err != nil { 74 | t.Fatal(err) 75 | } 76 | 77 | if err := c2.Send("ECHO", "world"); err != nil { 78 | t.Fatal(err) 79 | } 80 | if err := c2.Flush(); err != nil { 81 | t.Fatal(err) 82 | } 83 | 84 | s, err := redis.String(c2.Receive()) 85 | if err != nil { 86 | t.Fatal(err) 87 | } 88 | if s != "world" { 89 | t.Fatalf("echo returned %q, want %q", s, "world") 90 | } 91 | c2.Close() 92 | } 93 | 94 | func BenchmarkConn(b *testing.B) { 95 | b.StopTimer() 96 | c, err := redistest.Dial() 97 | if err != nil { 98 | b.Fatalf("error connection to database, %v", err) 99 | } 100 | defer c.Close() 101 | b.StartTimer() 102 | 103 | for i := 0; i < b.N; i++ { 104 | if _, err := c.Do("PING"); err != nil { 105 | b.Fatal(err) 106 | } 107 | } 108 | } 109 | 110 | func BenchmarkConnMux(b *testing.B) { 111 | b.StopTimer() 112 | c, err := redistest.Dial() 113 | if err != nil { 114 | b.Fatalf("error connection to database, %v", err) 115 | } 116 | m := redisx.NewConnMux(c) 117 | defer m.Close() 118 | 119 | b.StartTimer() 120 | 121 | for i := 0; i < b.N; i++ { 122 | c := m.Get() 123 | if _, err := c.Do("PING"); err != nil { 124 | b.Fatal(err) 125 | } 126 | c.Close() 127 | } 128 | } 129 | 130 | func BenchmarkPool(b *testing.B) { 131 | b.StopTimer() 132 | 133 | p := redis.Pool{Dial: redistest.Dial, MaxIdle: 1} 134 | defer p.Close() 135 | 136 | // Fill the pool. 137 | c := p.Get() 138 | if err := c.Err(); err != nil { 139 | b.Fatal(err) 140 | } 141 | c.Close() 142 | 143 | b.StartTimer() 144 | 145 | for i := 0; i < b.N; i++ { 146 | c := p.Get() 147 | if _, err := c.Do("PING"); err != nil { 148 | b.Fatal(err) 149 | } 150 | c.Close() 151 | } 152 | } 153 | 154 | const numConcurrent = 10 155 | 156 | func BenchmarkConnMuxConcurrent(b *testing.B) { 157 | b.StopTimer() 158 | c, err := redistest.Dial() 159 | if err != nil { 160 | b.Fatalf("error connection to database, %v", err) 161 | } 162 | defer c.Close() 163 | 164 | m := redisx.NewConnMux(c) 165 | 166 | var wg sync.WaitGroup 167 | wg.Add(numConcurrent) 168 | 169 | b.StartTimer() 170 | 171 | for i := 0; i < numConcurrent; i++ { 172 | go func() { 173 | defer wg.Done() 174 | for i := 0; i < b.N; i++ { 175 | c := m.Get() 176 | if _, err := c.Do("PING"); err != nil { 177 | b.Fatal(err) 178 | } 179 | c.Close() 180 | } 181 | }() 182 | } 183 | wg.Wait() 184 | } 185 | 186 | func BenchmarkPoolConcurrent(b *testing.B) { 187 | b.StopTimer() 188 | 189 | p := redis.Pool{Dial: redistest.Dial, MaxIdle: numConcurrent} 190 | defer p.Close() 191 | 192 | // Fill the pool. 193 | conns := make([]redis.Conn, numConcurrent) 194 | for i := range conns { 195 | c := p.Get() 196 | if err := c.Err(); err != nil { 197 | b.Fatal(err) 198 | } 199 | conns[i] = c 200 | } 201 | for _, c := range conns { 202 | c.Close() 203 | } 204 | 205 | var wg sync.WaitGroup 206 | wg.Add(numConcurrent) 207 | 208 | b.StartTimer() 209 | 210 | for i := 0; i < numConcurrent; i++ { 211 | go func() { 212 | defer wg.Done() 213 | for i := 0; i < b.N; i++ { 214 | c := p.Get() 215 | if _, err := c.Do("PING"); err != nil { 216 | b.Fatal(err) 217 | } 218 | c.Close() 219 | } 220 | }() 221 | } 222 | wg.Wait() 223 | } 224 | 225 | func BenchmarkPipelineConcurrency(b *testing.B) { 226 | b.StopTimer() 227 | c, err := redistest.Dial() 228 | if err != nil { 229 | b.Fatalf("error connection to database, %v", err) 230 | } 231 | defer c.Close() 232 | 233 | var wg sync.WaitGroup 234 | wg.Add(numConcurrent) 235 | 236 | var pipeline textproto.Pipeline 237 | 238 | b.StartTimer() 239 | 240 | for i := 0; i < numConcurrent; i++ { 241 | go func() { 242 | defer wg.Done() 243 | for i := 0; i < b.N; i++ { 244 | id := pipeline.Next() 245 | pipeline.StartRequest(id) 246 | c.Send("PING") 247 | c.Flush() 248 | pipeline.EndRequest(id) 249 | pipeline.StartResponse(id) 250 | _, err := c.Receive() 251 | if err != nil { 252 | b.Fatal(err) 253 | } 254 | pipeline.EndResponse(id) 255 | } 256 | }() 257 | } 258 | wg.Wait() 259 | } 260 | -------------------------------------------------------------------------------- /redis/asynconn.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 xiaofei, gistao 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package redis 16 | 17 | import ( 18 | "errors" 19 | "time" 20 | ) 21 | 22 | type tResult struct { 23 | result interface{} 24 | err error 25 | } 26 | 27 | // send to dorequest 28 | type tRequest struct { 29 | cmd string 30 | args []interface{} 31 | c chan *tResult 32 | } 33 | 34 | // send to doreply 35 | type tReply struct { 36 | cmd string 37 | c chan *tResult 38 | } 39 | 40 | type asyncRet struct { 41 | c chan *tResult 42 | } 43 | 44 | // conn is the low-level implementation of Conn 45 | type asynConn struct { 46 | *conn 47 | t time.Time 48 | reqChan chan *tRequest 49 | repChan chan *tReply 50 | closeReqChan chan bool 51 | closeRepChan chan bool 52 | closed bool 53 | } 54 | 55 | // AsyncDialTimeout acts like AsyncDial but takes timeouts for establishing the 56 | // connection to the server, writing a command and reading a reply. 57 | // 58 | // Deprecated: Use AsyncDial with options instead. 59 | func AsyncDialTimeout(network, address string, connectTimeout, readTimeout, writeTimeout time.Duration) (AsynConn, error) { 60 | return AsyncDial(network, address, 61 | DialConnectTimeout(connectTimeout), 62 | DialReadTimeout(readTimeout), 63 | DialWriteTimeout(writeTimeout)) 64 | } 65 | 66 | // AsyncDial connects to the Redis server at the given network and 67 | // address using the specified options. 68 | func AsyncDial(network, address string, options ...DialOption) (AsynConn, error) { 69 | tmp, err := Dial(network, address, options...) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | return getAsynConn(tmp.(*conn)) 75 | } 76 | 77 | // AsyncDialURL connects to a Redis server at the given URL using the Redis 78 | // URI scheme. URLs should follow the draft IANA specification for the 79 | // scheme (https://www.iana.org/assignments/uri-schemes/prov/redis). 80 | func AsyncDialURL(rawurl string, options ...DialOption) (AsynConn, error) { 81 | tmp, err := DialURL(rawurl, options...) 82 | if err != nil { 83 | return nil, err 84 | } 85 | return getAsynConn(tmp.(*conn)) 86 | } 87 | 88 | func getAsynConn(conn *conn) (AsynConn, error) { 89 | c := &asynConn{ 90 | conn: conn, 91 | reqChan: make(chan *tRequest, 1000), 92 | repChan: make(chan *tReply, 1000), 93 | closeReqChan: make(chan bool), 94 | closeRepChan: make(chan bool)} 95 | 96 | // request routine 97 | go c.doRequest() 98 | // reply routine 99 | go c.doReply() 100 | 101 | return c, nil 102 | } 103 | 104 | // Do command to redis server,the goroutine of caller will be suspended. 105 | func (c *asynConn) Do(cmd string, args ...interface{}) (interface{}, error) { 106 | if cmd == "" { 107 | return nil, errors.New("RedisGo-Async: empty command") 108 | } 109 | 110 | retChan := make(chan *tResult, 2) 111 | 112 | c.reqChan <- &tRequest{cmd: cmd, args: args, c: retChan} 113 | 114 | ret := <-retChan 115 | if ret.err != nil { 116 | return ret.result, ret.err 117 | } 118 | ret = <-retChan 119 | return ret.result, ret.err 120 | } 121 | 122 | // Do command to redis server,the goroutine of caller is not suspended. 123 | func (c *asynConn) AsyncDo(cmd string, args ...interface{}) (AsyncRet, error) { 124 | if cmd == "" { 125 | return nil, errors.New("RedisGo-Async: empty command") 126 | } 127 | 128 | retChan := make(chan *tResult, 2) 129 | 130 | c.reqChan <- &tRequest{cmd: cmd, args: args, c: retChan} 131 | 132 | return &asyncRet{c: retChan}, nil 133 | } 134 | 135 | func (c *asynConn) Close() error { 136 | c.mu.Lock() 137 | defer c.mu.Unlock() 138 | 139 | if c.closed { 140 | return nil 141 | } 142 | c.closed = true 143 | 144 | go func() { 145 | time.Sleep(10 * time.Minute) 146 | c.closeReqChan <- true 147 | }() 148 | 149 | c.err = errors.New("RedisGo-Async: closed") 150 | return c.conn.conn.Close() 151 | } 152 | 153 | func (c *asynConn) doRequest() { 154 | for { 155 | select { 156 | case <-c.closeReqChan: 157 | close(c.reqChan) 158 | c.closeRepChan <- true 159 | return 160 | 161 | case req := <-c.reqChan: 162 | for i, length := 0, len(c.reqChan); ; { 163 | if c.writeTimeout != 0 { 164 | c.conn.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout)) 165 | } 166 | if err := c.writeCommand(req.cmd, req.args); err != nil { 167 | req.c <- &tResult{nil, err} 168 | c.fatal(err) 169 | break 170 | } 171 | req.c <- &tResult{nil, nil} 172 | c.repChan <- &tReply{cmd: req.cmd, c: req.c} 173 | if i++; i > length { 174 | break 175 | } 176 | req = <-c.reqChan 177 | } 178 | } 179 | 180 | if err := c.bw.Flush(); err != nil { 181 | c.fatal(err) 182 | continue 183 | } 184 | } 185 | } 186 | 187 | func (c *asynConn) doReply() { 188 | for { 189 | select { 190 | case <-c.closeRepChan: 191 | close(c.repChan) 192 | return 193 | 194 | case rep := <-c.repChan: 195 | if c.readTimeout != 0 { 196 | c.conn.conn.SetReadDeadline(time.Now().Add(c.readTimeout)) 197 | } 198 | reply, err := c.readReply() 199 | if err != nil { 200 | rep.c <- &tResult{nil, err} 201 | c.fatal(err) 202 | continue 203 | } else { 204 | c.t = nowFunc() 205 | } 206 | if e, ok := reply.(Error); ok { 207 | err = e 208 | } 209 | rep.c <- &tResult{reply, err} 210 | } 211 | } 212 | } 213 | 214 | // Get get command result asynchronously 215 | func (a *asyncRet) Get() (interface{}, error) { 216 | send := <-a.c 217 | if send.err != nil { 218 | return send.result, send.err 219 | } 220 | 221 | recv := <-a.c 222 | return recv.result, recv.err 223 | } 224 | -------------------------------------------------------------------------------- /bench/benchresult.txt: -------------------------------------------------------------------------------- 1 | <----------------------------------------------------cross idc 2 | [root@yhg192 asyncbench]# go run asyncbench.go 3 | == Bench RedisGo-Async == 1000 goroutines 1 AsyncClient -- 10000 opts each --- 4 | 5 | [RedisGo-Async|async bench] 2017/08/11 14:48:12 --- 6 | [RedisGo-Async|async bench] 2017/08/11 14:48:12 cmd: SET 7 | [RedisGo-Async|async bench] 2017/08/11 14:48:12 1000 goroutines 1 asyncClient 10000000 iterations of SET in 28125 msecs 8 | [RedisGo-Async|async bench] 2017/08/11 14:48:12 --- 9 | 10 | [RedisGo-Async|async bench] 2017/08/11 14:48:42 --- 11 | [RedisGo-Async|async bench] 2017/08/11 14:48:42 cmd: GET 12 | [RedisGo-Async|async bench] 2017/08/11 14:48:42 1000 goroutines 1 asyncClient 10000000 iterations of GET in 29519 msecs 13 | [RedisGo-Async|async bench] 2017/08/11 14:48:42 --- 14 | 15 | [RedisGo-Async|async bench] 2017/08/11 14:49:10 --- 16 | [RedisGo-Async|async bench] 2017/08/11 14:49:10 cmd: DEL 17 | [RedisGo-Async|async bench] 2017/08/11 14:49:10 1000 goroutines 1 asyncClient 10000000 iterations of DEL in 27918 msecs 18 | [RedisGo-Async|async bench] 2017/08/11 14:49:10 --- 19 | 20 | [root@yhg192 syncbench]# go run syncbench.go 21 | == Bench RedisGo-Async == 1000 goroutines max 100 SyncClient -- 10000 opts each --- 22 | 23 | [RedisGo-Async|sync bench] 2017/08/11 14:54:07 --- 24 | [RedisGo-Async|sync bench] 2017/08/11 14:54:07 cmd: SET 25 | [RedisGo-Async|sync bench] 2017/08/11 14:54:07 1000 goroutines max 100 syncClient 10000000 iterations of SET in 113115 msecs 26 | [RedisGo-Async|sync bench] 2017/08/11 14:54:07 --- 27 | 28 | [RedisGo-Async|sync bench] 2017/08/11 14:56:01 --- 29 | [RedisGo-Async|sync bench] 2017/08/11 14:56:01 cmd: GET 30 | [RedisGo-Async|sync bench] 2017/08/11 14:56:01 1000 goroutines max 100 syncClient 10000000 iterations of GET in 113236 msecs 31 | [RedisGo-Async|sync bench] 2017/08/11 14:56:01 --- 32 | 33 | [RedisGo-Async|sync bench] 2017/08/11 14:57:54 --- 34 | [RedisGo-Async|sync bench] 2017/08/11 14:57:54 cmd: DEL 35 | [RedisGo-Async|sync bench] 2017/08/11 14:57:54 1000 goroutines max 100 syncClient 10000000 iterations of DEL in 113838 msecs 36 | [RedisGo-Async|sync bench] 2017/08/11 14:57:54 --- 37 | 38 | <----------------------------------------------------local host 39 | [root@yhg192 asyncbench]# go run asyncbench.go 40 | == Bench RedisGo-Async == 1000 goroutines 1 AsyncClient -- 10000 opts each --- 41 | 42 | [RedisGo-Async|async bench] 2017/08/11 15:03:55 --- 43 | [RedisGo-Async|async bench] 2017/08/11 15:03:55 cmd: SET 44 | [RedisGo-Async|async bench] 2017/08/11 15:03:55 1000 goroutines 1 asyncClient 10000000 iterations of SET in 30052 msecs 45 | [RedisGo-Async|async bench] 2017/08/11 15:03:55 --- 46 | 47 | [RedisGo-Async|async bench] 2017/08/11 15:04:26 --- 48 | [RedisGo-Async|async bench] 2017/08/11 15:04:26 cmd: GET 49 | [RedisGo-Async|async bench] 2017/08/11 15:04:26 1000 goroutines 1 asyncClient 10000000 iterations of GET in 31396 msecs 50 | [RedisGo-Async|async bench] 2017/08/11 15:04:26 --- 51 | 52 | [RedisGo-Async|async bench] 2017/08/11 15:04:54 --- 53 | [RedisGo-Async|async bench] 2017/08/11 15:04:54 cmd: DEL 54 | [RedisGo-Async|async bench] 2017/08/11 15:04:54 1000 goroutines 1 asyncClient 10000000 iterations of DEL in 27999 msecs 55 | [RedisGo-Async|async bench] 2017/08/11 15:04:54 --- 56 | 57 | [root@yhg192 syncbench]# go run syncbench.go 58 | == Bench RedisGo-Async == 1000 goroutines max 100 SyncClient -- 10000 opts each --- 59 | 60 | [RedisGo-Async|sync bench] 2017/08/11 15:00:06 --- 61 | [RedisGo-Async|sync bench] 2017/08/11 15:00:06 cmd: SET 62 | [RedisGo-Async|sync bench] 2017/08/11 15:00:06 1000 goroutines max 100 syncClient 10000000 iterations of SET in 80940 msecs 63 | [RedisGo-Async|sync bench] 2017/08/11 15:00:06 --- 64 | 65 | [RedisGo-Async|sync bench] 2017/08/11 15:01:27 --- 66 | [RedisGo-Async|sync bench] 2017/08/11 15:01:27 cmd: GET 67 | [RedisGo-Async|sync bench] 2017/08/11 15:01:27 1000 goroutines max 100 syncClient 10000000 iterations of GET in 80731 msecs 68 | [RedisGo-Async|sync bench] 2017/08/11 15:01:27 --- 69 | 70 | [RedisGo-Async|sync bench] 2017/08/11 15:02:45 --- 71 | [RedisGo-Async|sync bench] 2017/08/11 15:02:45 cmd: DEL 72 | [RedisGo-Async|sync bench] 2017/08/11 15:02:45 1000 goroutines max 100 syncClient 10000000 iterations of DEL in 77966 msecs 73 | [RedisGo-Async|sync bench] 2017/08/11 15:02:45 --- 74 | 75 | 76 | <----------------------------------------------------redis-benchmark local host 77 | [root@yhg192 src]# ./redis-benchmark -n 10000000 -t set,get 78 | ====== SET ====== 79 | 10000000 requests completed in 106.97 seconds 80 | 50 parallel clients 81 | 3 bytes payload 82 | keep alive: 1 83 | 84 | 100.00% <= 1 milliseconds 85 | 100.00% <= 2 milliseconds 86 | 100.00% <= 3 milliseconds 87 | 100.00% <= 3 milliseconds 88 | 93484.16 requests per second 89 | 90 | ====== GET ====== 91 | 10000000 requests completed in 108.85 seconds 92 | 50 parallel clients 93 | 3 bytes payload 94 | keep alive: 1 95 | 96 | 100.00% <= 1 milliseconds 97 | 100.00% <= 2 milliseconds 98 | 100.00% <= 3 milliseconds 99 | 100.00% <= 3 milliseconds 100 | 91872.08 requests per second 101 | 102 | <----------------------------------------------------redis-benchmark cross idc 103 | [root@yhg192 src]# ./redis-benchmark -h 10.13.112.152 -p 6379 -n 10000000 -t set,get 104 | ====== SET ====== 105 | 10000000 requests completed in 205.67 seconds 106 | 50 parallel clients 107 | 3 bytes payload 108 | keep alive: 1 109 | 110 | 62.65% <= 1 milliseconds 111 | 99.79% <= 2 milliseconds 112 | 99.86% <= 3 milliseconds 113 | 99.91% <= 4 milliseconds 114 | 99.93% <= 5 milliseconds 115 | 99.95% <= 6 milliseconds 116 | 99.96% <= 7 milliseconds 117 | 99.97% <= 8 milliseconds 118 | 99.98% <= 9 milliseconds 119 | 99.99% <= 10 milliseconds 120 | 99.99% <= 11 milliseconds 121 | 100.00% <= 12 milliseconds 122 | 100.00% <= 201 milliseconds 123 | 100.00% <= 202 milliseconds 124 | 100.00% <= 202 milliseconds 125 | 48621.11 requests per second 126 | 127 | ====== GET ====== 128 | 10000000 requests completed in 204.60 seconds 129 | 50 parallel clients 130 | 3 bytes payload 131 | keep alive: 1 132 | 133 | 66.07% <= 1 milliseconds 134 | 99.87% <= 2 milliseconds 135 | 99.93% <= 3 milliseconds 136 | 99.96% <= 4 milliseconds 137 | 99.98% <= 5 milliseconds 138 | 99.99% <= 6 milliseconds 139 | 99.99% <= 7 milliseconds 140 | 99.99% <= 8 milliseconds 141 | 100.00% <= 9 milliseconds 142 | 100.00% <= 10 milliseconds 143 | 100.00% <= 11 milliseconds 144 | 100.00% <= 12 milliseconds 145 | 100.00% <= 201 milliseconds 146 | 100.00% <= 202 milliseconds 147 | 100.00% <= 203 milliseconds 148 | 48874.90 requests per second 149 | -------------------------------------------------------------------------------- /redis/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Gary Burd 2 | // Copyright 2017 gistao 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 5 | // not use this file except in compliance with the License. You may obtain 6 | // a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | // License for the specific language governing permissions and limitations 14 | // under the License. 15 | 16 | // Package redis is a client for the Redis database.both asynchronous and synchronous modes 17 | // are supported,its API is fully compatible with redigo. 18 | // 19 | // The RedisGo-Async FAQ (https://github.com/gistao/RedisGo-Async/wiki/FAQ) contains more 20 | // documentation about this package. 21 | // 22 | // Connections 23 | // 24 | // In synchronous mode, this library creates a connection pool, 25 | // and then you can test to determine a maximum number of connections, 26 | // such as 100. 27 | // 28 | // In asynchronous mode, this library will only create a connection, 29 | // and you don't have to worry about performance issues, 30 | // nor do you have to spend a lot of time testing the number of connections. 31 | // 32 | // Executing Commands 33 | // 34 | // The Conn interface has a generic method for executing Redis commands: 35 | // 36 | // Do(commandName string, args ...interface{}) (reply interface{}, err error) 37 | // 38 | // The Redis command reference (http://redis.io/commands) lists the available 39 | // commands. An example of using the Redis APPEND command is: 40 | // 41 | // n, err := conn.Do("APPEND", "key", "value") 42 | // 43 | // The Do method converts command arguments to binary strings for transmission 44 | // to the server as follows: 45 | // 46 | // Go Type Conversion 47 | // []byte Sent as is 48 | // string Sent as is 49 | // int, int64 strconv.FormatInt(v) 50 | // float64 strconv.FormatFloat(v, 'g', -1, 64) 51 | // bool true -> "1", false -> "0" 52 | // nil "" 53 | // all other types fmt.Print(v) 54 | // 55 | // Redis command reply types are represented using the following Go types: 56 | // 57 | // Redis type Go type 58 | // error redis.Error 59 | // integer int64 60 | // simple string string 61 | // bulk string []byte or nil if value not present. 62 | // array []interface{} or nil if value not present. 63 | // 64 | // Use type assertions or the reply helper functions to convert from 65 | // interface{} to the specific Go type for the command result. 66 | // 67 | // Pipelining 68 | // 69 | // In synchronous mode, Connections support pipelining using the Send, Flush and Receive methods. 70 | // 71 | // Send(commandName string, args ...interface{}) error 72 | // Flush() error 73 | // Receive() (reply interface{}, err error) 74 | // 75 | // Send writes the command to the connection's output buffer. Flush flushes the 76 | // connection's output buffer to the server. Receive reads a single reply from 77 | // the server. The following example shows a simple pipeline. 78 | // 79 | // c.Send("SET", "foo", "bar") 80 | // c.Send("GET", "foo") 81 | // c.Flush() 82 | // c.Receive() // reply from SET 83 | // v, err = c.Receive() // reply from GET 84 | // 85 | // In asynchronous mode, Connections support pipelining using the Do 86 | // 87 | // AsyncDo(commandName string, args ...interface{}) (reply interface{}, err error) 88 | // 89 | // Above example 90 | // 91 | // c.AsyncDo("SET", "foo", "bar") 92 | // ret := c.AsyncDo("Get", "foo") 93 | // v, err := ret.Get() 94 | // 95 | // Concurrency 96 | // 97 | // Connections support one concurrent caller to the Receive method and one 98 | // concurrent caller to the Send and Flush methods. No other concurrency is 99 | // supported including concurrent calls to the Do method. 100 | // 101 | // For full concurrent access to Redis, use the thread-safe Pool to get. 102 | // 103 | // Publish and Subscribe 104 | // 105 | // Use the Send, Flush and Receive methods to implement Pub/Sub subscribers. 106 | // 107 | // c.Send("SUBSCRIBE", "example") 108 | // c.Flush() 109 | // for { 110 | // reply, err := c.Receive() 111 | // if err != nil { 112 | // return err 113 | // } 114 | // // process pushed message 115 | // } 116 | // 117 | // The PubSubConn type wraps a Conn with convenience methods for implementing 118 | // subscribers. The Subscribe, PSubscribe, Unsubscribe and PUnsubscribe methods 119 | // send and flush a subscription management command. The receive method 120 | // converts a pushed message to convenient types for use in a type switch. 121 | // 122 | // psc := redis.PubSubConn{Conn: c} 123 | // psc.Subscribe("example") 124 | // for { 125 | // switch v := psc.Receive().(type) { 126 | // case redis.Message: 127 | // fmt.Printf("%s: message: %s\n", v.Channel, v.Data) 128 | // case redis.Subscription: 129 | // fmt.Printf("%s: %s %d\n", v.Channel, v.Kind, v.Count) 130 | // case error: 131 | // return v 132 | // } 133 | // } 134 | // 135 | // NOTE asynchronous mode does not support PUB/SUB 136 | // 137 | // Reply Helpers 138 | // 139 | // The Bool, Int, Bytes, String, Strings and Values functions convert a reply 140 | // to a value of a specific type. To allow convenient wrapping of calls to the 141 | // connection Do and Receive methods, the functions take a second argument of 142 | // type error. If the error is non-nil, then the helper function returns the 143 | // error. If the error is nil, the function converts the reply to the specified 144 | // type: 145 | // 146 | // exists, err := redis.Bool(c.Do("EXISTS", "foo")) 147 | // if err != nil { 148 | // // handle error return from c.Do or type conversion error. 149 | // } 150 | // 151 | // The Scan function converts elements of a array reply to Go types: 152 | // 153 | // var value1 int 154 | // var value2 string 155 | // reply, err := redis.Values(c.Do("MGET", "key1", "key2")) 156 | // if err != nil { 157 | // // handle error 158 | // } 159 | // if _, err := redis.Scan(reply, &value1, &value2); err != nil { 160 | // // handle error 161 | // } 162 | // 163 | // Errors 164 | // 165 | // Connection methods return error replies from the server as type redis.Error. 166 | // 167 | // Call the connection Err() method to determine if the connection encountered 168 | // non-recoverable error such as a network error or protocol parsing error. If 169 | // Err() returns a non-nil value, then the connection is not usable and should 170 | // be closed. 171 | package redis 172 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 gistao, xiaofei 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "github.com/gistao/RedisGo-Async/redis" 19 | "log" 20 | "sync" 21 | "time" 22 | ) 23 | 24 | type RedisClient struct { 25 | // pool *redis.Pool 26 | pool *redis.AsyncPool 27 | Addr string 28 | } 29 | 30 | var ( 31 | cliMap map[string]*RedisClient 32 | mutex *sync.RWMutex 33 | ) 34 | 35 | func init() { 36 | cliMap = make(map[string]*RedisClient) 37 | mutex = new(sync.RWMutex) 38 | } 39 | 40 | func newSyncPool(addr string) *redis.Pool { 41 | return &redis.Pool{ 42 | MaxIdle: 100, 43 | MaxActive: 100, 44 | IdleTimeout: time.Minute * 1, 45 | Wait: true, 46 | Dial: func() (redis.Conn, error) { 47 | c, err := redis.Dial("tcp", addr) 48 | return c, err 49 | }, 50 | } 51 | } 52 | 53 | func newAsyncPool(addr string) *redis.AsyncPool { 54 | return &redis.AsyncPool{ 55 | Dial: func() (redis.AsynConn, error) { 56 | c, err := redis.AsyncDial("tcp", addr) 57 | return c, err 58 | }, 59 | MaxGetCount: 1000, 60 | } 61 | } 62 | 63 | func GetRedisClient(addr string) *RedisClient { 64 | var redis *RedisClient 65 | var ok bool 66 | mutex.RLock() 67 | redis, ok = cliMap[addr] 68 | mutex.RUnlock() 69 | if !ok { 70 | mutex.Lock() 71 | redis, ok = cliMap[addr] 72 | if !ok { 73 | //redis = &RedisClient{pool: newSyncPool(addr), Addr: addr} 74 | redis = &RedisClient{pool: newAsyncPool(addr), Addr: addr} 75 | cliMap[addr] = redis 76 | } 77 | mutex.Unlock() 78 | } 79 | return redis 80 | } 81 | 82 | func (c *RedisClient) Exists(key string) (int64, error) { 83 | conn := c.pool.Get() 84 | defer conn.Close() 85 | reply, err := conn.Do("EXISTS", key) 86 | if err == nil && reply == nil { 87 | return 0, nil 88 | } 89 | val, err := redis.Int64(reply, err) 90 | return val, err 91 | } 92 | 93 | func (c *RedisClient) Get(key string) (string, error) { 94 | conn := c.pool.Get() 95 | defer conn.Close() 96 | reply, err := conn.Do("GET", key) 97 | if err == nil && reply == nil { 98 | return "", nil 99 | } 100 | val, err := redis.String(reply, err) 101 | return val, err 102 | } 103 | 104 | func (c *RedisClient) Del(key string) (int64, error) { 105 | conn := c.pool.Get() 106 | defer conn.Close() 107 | reply, err := conn.Do("DEL", key) 108 | if err == nil && reply == nil { 109 | return 0, nil 110 | } 111 | val, err := redis.Int64(reply, err) 112 | return val, err 113 | } 114 | 115 | func (c *RedisClient) HGet(hashID string, field string) (string, error) { 116 | conn := c.pool.Get() 117 | defer conn.Close() 118 | reply, err := conn.Do("HGET", hashID, field) 119 | if err == nil && reply == nil { 120 | return "", nil 121 | } 122 | val, err := redis.String(reply, err) 123 | return val, err 124 | } 125 | 126 | func (c *RedisClient) INCR(key string) (int, error) { 127 | conn := c.pool.Get() 128 | defer conn.Close() 129 | reply, err := conn.Do("INCR", key) 130 | if err == nil && reply == nil { 131 | return 0, nil 132 | } 133 | val, err := redis.Int(reply, err) 134 | return val, err 135 | } 136 | 137 | func (c *RedisClient) DECR(key string) (int, error) { 138 | conn := c.pool.Get() 139 | defer conn.Close() 140 | reply, err := conn.Do("DECR", key) 141 | if err == nil && reply == nil { 142 | return 0, nil 143 | } 144 | val, err := redis.Int(reply, err) 145 | return val, err 146 | } 147 | 148 | func (c *RedisClient) HGetAll(hashID string) (map[string]string, error) { 149 | conn := c.pool.Get() 150 | defer conn.Close() 151 | reply, err := redis.StringMap(conn.Do("HGetAll", hashID)) 152 | return reply, err 153 | } 154 | 155 | func (c *RedisClient) HSet(hashID string, field string, val string) error { 156 | conn := c.pool.Get() 157 | defer conn.Close() 158 | _, err := conn.Do("HSET", hashID, field, val) 159 | return err 160 | } 161 | 162 | func (c *RedisClient) HMSet(args ...interface{}) error { 163 | conn := c.pool.Get() 164 | defer conn.Close() 165 | _, err := conn.Do("HMSET", args...) 166 | return err 167 | } 168 | 169 | func (c *RedisClient) Expire(key string, expire int64) error { 170 | conn := c.pool.Get() 171 | defer conn.Close() 172 | _, err := conn.Do("EXPIRE", key, expire) 173 | return err 174 | } 175 | 176 | func (c *RedisClient) Set(key string, val string) (string, error) { 177 | conn := c.pool.Get() 178 | defer conn.Close() 179 | val, err := redis.String(conn.Do("SET", key, val)) 180 | return val, err 181 | } 182 | 183 | func (c *RedisClient) SetWithExpire(key string, val string, timeOutSeconds int) (string, error) { 184 | conn := c.pool.Get() 185 | defer conn.Close() 186 | val, err := redis.String(conn.Do("SET", key, val, "EX", timeOutSeconds)) 187 | return val, err 188 | } 189 | 190 | func (c *RedisClient) GetTTL(key string) (int64, error) { 191 | conn := c.pool.Get() 192 | defer conn.Close() 193 | val, err := redis.Int64(conn.Do("TTL", key)) 194 | return val, err 195 | } 196 | 197 | func (c *RedisClient) ZAdd(args ...interface{}) error { 198 | conn := c.pool.Get() 199 | defer conn.Close() 200 | _, err := conn.Do("ZADD", args...) 201 | return err 202 | } 203 | 204 | // list操作 205 | func (c *RedisClient) LLen(key string) (int64, error) { 206 | conn := c.pool.Get() 207 | defer conn.Close() 208 | val, err := redis.Int64(conn.Do("LLEN", key)) 209 | return val, err 210 | } 211 | 212 | func (c *RedisClient) RPopLPush(src, dst string) (string, error) { 213 | conn := c.pool.Get() 214 | defer conn.Close() 215 | val, err := redis.String(conn.Do("RPOPLPUSH", src, dst)) 216 | return val, err 217 | } 218 | 219 | func (c *RedisClient) RPop(key string) (string, error) { 220 | conn := c.pool.Get() 221 | defer conn.Close() 222 | val, err := redis.String(conn.Do("RPOP", key)) 223 | return val, err 224 | } 225 | 226 | func (c *RedisClient) LPop(key string) (string, error) { 227 | conn := c.pool.Get() 228 | defer conn.Close() 229 | val, err := redis.String(conn.Do("LPOP", key)) 230 | return val, err 231 | } 232 | 233 | func (c *RedisClient) RPush(key string, val string) (int64, error) { 234 | conn := c.pool.Get() 235 | defer conn.Close() 236 | ret, err := redis.Int64(conn.Do("RPUSH", key, val)) 237 | if err != nil { 238 | return -1, err 239 | } else { 240 | return ret, nil 241 | } 242 | } 243 | 244 | func (c *RedisClient) LPush(key string, val string) (int64, error) { 245 | conn := c.pool.Get() 246 | defer conn.Close() 247 | ret, err := redis.Int64(conn.Do("LPUSH", key, val)) 248 | if err != nil { 249 | return -1, err 250 | } else { 251 | return ret, nil 252 | } 253 | } 254 | 255 | func main() { 256 | log.SetPrefix("[RedisGo-Async|example] ") 257 | // get client 258 | rdc := GetRedisClient("127.0.0.1:6379") 259 | key := "hello-redisgo-async" 260 | val := "hello world" 261 | _, err := rdc.Set(key, val) 262 | if err != nil { 263 | log.Println(err) 264 | } 265 | log.Printf("set %s value \"%s\" success\n", key, val) 266 | 267 | ret, err := rdc.Get(key) 268 | if err != nil { 269 | log.Println(err) 270 | } 271 | log.Printf("get %s result \"%s\"\n", key, ret) 272 | 273 | _, err = rdc.Del(key) 274 | if err != nil { 275 | log.Println(err) 276 | } 277 | log.Printf("del key %s success\n", key) 278 | } 279 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /redis/pool.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Gary Burd 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package redis 16 | 17 | import ( 18 | "bytes" 19 | "container/list" 20 | "crypto/rand" 21 | "crypto/sha1" 22 | "errors" 23 | "io" 24 | "strconv" 25 | "sync" 26 | "time" 27 | 28 | "github.com/gistao/RedisGo-Async/internal" 29 | ) 30 | 31 | var nowFunc = time.Now // for testing 32 | 33 | // ErrPoolExhausted is returned from a pool connection method (Do, Send, 34 | // Receive, Flush, Err) when the maximum number of database connections in the 35 | // pool has been reached. 36 | var ErrPoolExhausted = errors.New("RedisGo-Async: connection pool exhausted") 37 | 38 | var ( 39 | errPoolClosed = errors.New("RedisGo-Async: connection pool closed") 40 | errConnClosed = errors.New("RedisGo-Async: connection closed") 41 | ) 42 | 43 | // Pool maintains a pool of connections. The application calls the Get method 44 | // to get a connection from the pool and the connection's Close method to 45 | // return the connection's resources to the pool. 46 | // 47 | // The following example shows how to use a pool in a web application. The 48 | // application creates a pool at application startup and makes it available to 49 | // request handlers using a package level variable. The pool configuration used 50 | // here is an example, not a recommendation. 51 | // 52 | // func newPool(addr string) *redis.Pool { 53 | // return &redis.Pool{ 54 | // MaxIdle: 3, 55 | // IdleTimeout: 240 * time.Second, 56 | // Dial: func () (redis.Conn, error) { return redis.Dial("tcp", addr) }, 57 | // } 58 | // } 59 | // 60 | // var ( 61 | // pool *redis.Pool 62 | // redisServer = flag.String("redisServer", ":6379", "") 63 | // ) 64 | // 65 | // func main() { 66 | // flag.Parse() 67 | // pool = newPool(*redisServer) 68 | // ... 69 | // } 70 | // 71 | // A request handler gets a connection from the pool and closes the connection 72 | // when the handler is done: 73 | // 74 | // func serveHome(w http.ResponseWriter, r *http.Request) { 75 | // conn := pool.Get() 76 | // defer conn.Close() 77 | // ... 78 | // } 79 | // 80 | // Use the Dial function to authenticate connections with the AUTH command or 81 | // select a database with the SELECT command: 82 | // 83 | // pool := &redis.Pool{ 84 | // // Other pool configuration not shown in this example. 85 | // Dial: func () (redis.Conn, error) { 86 | // c, err := redis.Dial("tcp", server) 87 | // if err != nil { 88 | // return nil, err 89 | // } 90 | // if _, err := c.Do("AUTH", password); err != nil { 91 | // c.Close() 92 | // return nil, err 93 | // } 94 | // if _, err := c.Do("SELECT", db); err != nil { 95 | // c.Close() 96 | // return nil, err 97 | // } 98 | // return c, nil 99 | // } 100 | // } 101 | // 102 | // Use the TestOnBorrow function to check the health of an idle connection 103 | // before the connection is returned to the application. This example PINGs 104 | // connections that have been idle more than a minute: 105 | // 106 | // pool := &redis.Pool{ 107 | // // Other pool configuration not shown in this example. 108 | // TestOnBorrow: func(c redis.Conn, t time.Time) error { 109 | // if time.Since(t) < time.Minute { 110 | // return nil 111 | // } 112 | // _, err := c.Do("PING") 113 | // return err 114 | // }, 115 | // } 116 | // 117 | type Pool struct { 118 | 119 | // Dial is an application supplied function for creating and configuring a 120 | // connection. 121 | // 122 | // The connection returned from Dial must not be in a special state 123 | // (subscribed to pubsub channel, transaction started, ...). 124 | Dial func() (Conn, error) 125 | 126 | // TestOnBorrow is an optional application supplied function for checking 127 | // the health of an idle connection before the connection is used again by 128 | // the application. Argument t is the time that the connection was returned 129 | // to the pool. If the function returns an error, then the connection is 130 | // closed. 131 | TestOnBorrow func(c Conn, t time.Time) error 132 | 133 | // Maximum number of idle connections in the pool. 134 | MaxIdle int 135 | 136 | // Maximum number of connections allocated by the pool at a given time. 137 | // When zero, there is no limit on the number of connections in the pool. 138 | MaxActive int 139 | 140 | // Close connections after remaining idle for this duration. If the value 141 | // is zero, then idle connections are not closed. Applications should set 142 | // the timeout to a value less than the server's timeout. 143 | IdleTimeout time.Duration 144 | 145 | // If Wait is true and the pool is at the MaxActive limit, then Get() waits 146 | // for a connection to be returned to the pool before returning. 147 | Wait bool 148 | 149 | // mu protects fields defined below. 150 | mu sync.Mutex 151 | cond *sync.Cond 152 | closed bool 153 | active int 154 | 155 | // Stack of idleConn with most recently used at the front. 156 | idle list.List 157 | } 158 | 159 | type idleConn struct { 160 | c Conn 161 | t time.Time 162 | } 163 | 164 | // NewPool creates a new pool. 165 | // 166 | // Deprecated: Initialize the Pool directory as shown in the example. 167 | func NewPool(newFn func() (Conn, error), maxIdle int) *Pool { 168 | return &Pool{Dial: newFn, MaxIdle: maxIdle} 169 | } 170 | 171 | // Get gets a connection. The application must close the returned connection. 172 | // This method always returns a valid connection so that applications can defer 173 | // error handling to the first use of the connection. If there is an error 174 | // getting an underlying connection, then the connection Err, Do, Send, Flush 175 | // and Receive methods return that error. 176 | func (p *Pool) Get() Conn { 177 | c, err := p.get() 178 | if err != nil { 179 | return errorConnection{err} 180 | } 181 | return &pooledConnection{p: p, c: c} 182 | } 183 | 184 | // ActiveCount returns the number of connections in the pool. The count includes idle connections and connections in use. 185 | func (p *Pool) ActiveCount() int { 186 | p.mu.Lock() 187 | active := p.active 188 | p.mu.Unlock() 189 | return active 190 | } 191 | 192 | // IdleCount returns the number of idle connections in the pool. 193 | func (p *Pool) IdleCount() int { 194 | p.mu.Lock() 195 | idle := p.idle.Len() 196 | p.mu.Unlock() 197 | return idle 198 | } 199 | 200 | // Close releases the resources used by the pool. 201 | func (p *Pool) Close() error { 202 | p.mu.Lock() 203 | idle := p.idle 204 | p.idle.Init() 205 | p.closed = true 206 | p.active -= idle.Len() 207 | if p.cond != nil { 208 | p.cond.Broadcast() 209 | } 210 | p.mu.Unlock() 211 | for e := idle.Front(); e != nil; e = e.Next() { 212 | e.Value.(idleConn).c.Close() 213 | } 214 | return nil 215 | } 216 | 217 | // release decrements the active count and signals waiters. The caller must 218 | // hold p.mu during the call. 219 | func (p *Pool) release() { 220 | p.active -= 1 221 | if p.cond != nil { 222 | p.cond.Signal() 223 | } 224 | } 225 | 226 | // get prunes stale connections and returns a connection from the idle list or 227 | // creates a new connection. 228 | func (p *Pool) get() (Conn, error) { 229 | p.mu.Lock() 230 | 231 | // Prune stale connections. 232 | 233 | if timeout := p.IdleTimeout; timeout > 0 { 234 | for i, n := 0, p.idle.Len(); i < n; i++ { 235 | e := p.idle.Back() 236 | if e == nil { 237 | break 238 | } 239 | ic := e.Value.(idleConn) 240 | if ic.t.Add(timeout).After(nowFunc()) { 241 | break 242 | } 243 | p.idle.Remove(e) 244 | p.release() 245 | p.mu.Unlock() 246 | ic.c.Close() 247 | p.mu.Lock() 248 | } 249 | } 250 | 251 | for { 252 | 253 | // Get idle connection. 254 | 255 | for i, n := 0, p.idle.Len(); i < n; i++ { 256 | e := p.idle.Front() 257 | if e == nil { 258 | break 259 | } 260 | ic := e.Value.(idleConn) 261 | p.idle.Remove(e) 262 | test := p.TestOnBorrow 263 | p.mu.Unlock() 264 | if test == nil || test(ic.c, ic.t) == nil { 265 | return ic.c, nil 266 | } 267 | ic.c.Close() 268 | p.mu.Lock() 269 | p.release() 270 | } 271 | 272 | // Check for pool closed before dialing a new connection. 273 | 274 | if p.closed { 275 | p.mu.Unlock() 276 | return nil, errors.New("RedisGo-Async: get on closed pool") 277 | } 278 | 279 | // Dial new connection if under limit. 280 | 281 | if p.MaxActive == 0 || p.active < p.MaxActive { 282 | dial := p.Dial 283 | p.active += 1 284 | p.mu.Unlock() 285 | c, err := dial() 286 | if err != nil { 287 | p.mu.Lock() 288 | p.release() 289 | p.mu.Unlock() 290 | c = nil 291 | } 292 | return c, err 293 | } 294 | 295 | if !p.Wait { 296 | p.mu.Unlock() 297 | return nil, ErrPoolExhausted 298 | } 299 | 300 | if p.cond == nil { 301 | p.cond = sync.NewCond(&p.mu) 302 | } 303 | p.cond.Wait() 304 | } 305 | } 306 | 307 | func (p *Pool) put(c Conn, forceClose bool) error { 308 | err := c.Err() 309 | p.mu.Lock() 310 | if !p.closed && err == nil && !forceClose { 311 | p.idle.PushFront(idleConn{t: nowFunc(), c: c}) 312 | if p.idle.Len() > p.MaxIdle { 313 | c = p.idle.Remove(p.idle.Back()).(idleConn).c 314 | } else { 315 | c = nil 316 | } 317 | } 318 | 319 | if c == nil { 320 | if p.cond != nil { 321 | p.cond.Signal() 322 | } 323 | p.mu.Unlock() 324 | return nil 325 | } 326 | 327 | p.release() 328 | p.mu.Unlock() 329 | return c.Close() 330 | } 331 | 332 | type pooledConnection struct { 333 | p *Pool 334 | c Conn 335 | state int 336 | } 337 | 338 | var ( 339 | sentinel []byte 340 | sentinelOnce sync.Once 341 | ) 342 | 343 | func initSentinel() { 344 | p := make([]byte, 64) 345 | if _, err := rand.Read(p); err == nil { 346 | sentinel = p 347 | } else { 348 | h := sha1.New() 349 | io.WriteString(h, "Oops, rand failed. Use time instead.") 350 | io.WriteString(h, strconv.FormatInt(time.Now().UnixNano(), 10)) 351 | sentinel = h.Sum(nil) 352 | } 353 | } 354 | 355 | func (pc *pooledConnection) Close() error { 356 | c := pc.c 357 | if _, ok := c.(errorConnection); ok { 358 | return nil 359 | } 360 | pc.c = errorConnection{errConnClosed} 361 | 362 | if pc.state&internal.MultiState != 0 { 363 | c.Send("DISCARD") 364 | pc.state &^= (internal.MultiState | internal.WatchState) 365 | } else if pc.state&internal.WatchState != 0 { 366 | c.Send("UNWATCH") 367 | pc.state &^= internal.WatchState 368 | } 369 | if pc.state&internal.SubscribeState != 0 { 370 | c.Send("UNSUBSCRIBE") 371 | c.Send("PUNSUBSCRIBE") 372 | // To detect the end of the message stream, ask the server to echo 373 | // a sentinel value and read until we see that value. 374 | sentinelOnce.Do(initSentinel) 375 | c.Send("ECHO", sentinel) 376 | c.Flush() 377 | for { 378 | p, err := c.Receive() 379 | if err != nil { 380 | break 381 | } 382 | if p, ok := p.([]byte); ok && bytes.Equal(p, sentinel) { 383 | pc.state &^= internal.SubscribeState 384 | break 385 | } 386 | } 387 | } 388 | c.Do("") 389 | pc.p.put(c, pc.state != 0) 390 | return nil 391 | } 392 | 393 | func (pc *pooledConnection) Err() error { 394 | return pc.c.Err() 395 | } 396 | 397 | func (pc *pooledConnection) Do(commandName string, args ...interface{}) (reply interface{}, err error) { 398 | ci := internal.LookupCommandInfo(commandName) 399 | pc.state = (pc.state | ci.Set) &^ ci.Clear 400 | return pc.c.Do(commandName, args...) 401 | } 402 | 403 | func (pc *pooledConnection) Send(commandName string, args ...interface{}) error { 404 | ci := internal.LookupCommandInfo(commandName) 405 | pc.state = (pc.state | ci.Set) &^ ci.Clear 406 | return pc.c.Send(commandName, args...) 407 | } 408 | 409 | func (pc *pooledConnection) Flush() error { 410 | return pc.c.Flush() 411 | } 412 | 413 | func (pc *pooledConnection) Receive() (reply interface{}, err error) { 414 | return pc.c.Receive() 415 | } 416 | 417 | type errorConnection struct{ err error } 418 | 419 | func (ec errorConnection) Do(string, ...interface{}) (interface{}, error) { return nil, ec.err } 420 | func (ec errorConnection) Send(string, ...interface{}) error { return ec.err } 421 | func (ec errorConnection) Err() error { return ec.err } 422 | func (ec errorConnection) Close() error { return ec.err } 423 | func (ec errorConnection) Flush() error { return ec.err } 424 | func (ec errorConnection) Receive() (interface{}, error) { return nil, ec.err } 425 | -------------------------------------------------------------------------------- /redis/scan_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Gary Burd 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package redis_test 16 | 17 | import ( 18 | "fmt" 19 | "math" 20 | "reflect" 21 | "testing" 22 | "time" 23 | 24 | "github.com/gistao/RedisGo-Async/redis" 25 | ) 26 | 27 | type durationScan struct { 28 | time.Duration `redis:"sd"` 29 | } 30 | 31 | func (t *durationScan) RedisScan(src interface{}) (err error) { 32 | if t == nil { 33 | return fmt.Errorf("nil pointer") 34 | } 35 | switch src := src.(type) { 36 | case string: 37 | t.Duration, err = time.ParseDuration(src) 38 | case []byte: 39 | t.Duration, err = time.ParseDuration(string(src)) 40 | case int64: 41 | t.Duration = time.Duration(src) 42 | default: 43 | err = fmt.Errorf("cannot convert from %T to %T", src, t) 44 | } 45 | return err 46 | } 47 | 48 | var scanConversionTests = []struct { 49 | src interface{} 50 | dest interface{} 51 | }{ 52 | {[]byte("-inf"), math.Inf(-1)}, 53 | {[]byte("+inf"), math.Inf(1)}, 54 | {[]byte("0"), float64(0)}, 55 | {[]byte("3.14159"), float64(3.14159)}, 56 | {[]byte("3.14"), float32(3.14)}, 57 | {[]byte("-100"), int(-100)}, 58 | {[]byte("101"), int(101)}, 59 | {int64(102), int(102)}, 60 | {[]byte("103"), uint(103)}, 61 | {int64(104), uint(104)}, 62 | {[]byte("105"), int8(105)}, 63 | {int64(106), int8(106)}, 64 | {[]byte("107"), uint8(107)}, 65 | {int64(108), uint8(108)}, 66 | {[]byte("0"), false}, 67 | {int64(0), false}, 68 | {[]byte("f"), false}, 69 | {[]byte("1"), true}, 70 | {int64(1), true}, 71 | {[]byte("t"), true}, 72 | {"hello", "hello"}, 73 | {[]byte("hello"), "hello"}, 74 | {[]byte("world"), []byte("world")}, 75 | {[]interface{}{[]byte("foo")}, []interface{}{[]byte("foo")}}, 76 | {[]interface{}{[]byte("foo")}, []string{"foo"}}, 77 | {[]interface{}{[]byte("hello"), []byte("world")}, []string{"hello", "world"}}, 78 | {[]interface{}{[]byte("bar")}, [][]byte{[]byte("bar")}}, 79 | {[]interface{}{[]byte("1")}, []int{1}}, 80 | {[]interface{}{[]byte("1"), []byte("2")}, []int{1, 2}}, 81 | {[]interface{}{[]byte("1"), []byte("2")}, []float64{1, 2}}, 82 | {[]interface{}{[]byte("1")}, []byte{1}}, 83 | {[]interface{}{[]byte("1")}, []bool{true}}, 84 | {"1m", durationScan{Duration: time.Minute}}, 85 | {[]byte("1m"), durationScan{Duration: time.Minute}}, 86 | {time.Minute.Nanoseconds(), durationScan{Duration: time.Minute}}, 87 | {[]interface{}{[]byte("1m")}, []durationScan{durationScan{Duration: time.Minute}}}, 88 | {[]interface{}{[]byte("1m")}, []*durationScan{&durationScan{Duration: time.Minute}}}, 89 | } 90 | 91 | func TestScanConversion(t *testing.T) { 92 | for _, tt := range scanConversionTests { 93 | values := []interface{}{tt.src} 94 | dest := reflect.New(reflect.TypeOf(tt.dest)) 95 | values, err := redis.Scan(values, dest.Interface()) 96 | if err != nil { 97 | t.Errorf("Scan(%v) returned error %v", tt, err) 98 | continue 99 | } 100 | if !reflect.DeepEqual(tt.dest, dest.Elem().Interface()) { 101 | t.Errorf("Scan(%v) returned %v, want %v", tt, dest.Elem().Interface(), tt.dest) 102 | } 103 | } 104 | } 105 | 106 | var scanConversionErrorTests = []struct { 107 | src interface{} 108 | dest interface{} 109 | }{ 110 | {[]byte("1234"), byte(0)}, 111 | {int64(1234), byte(0)}, 112 | {[]byte("-1"), byte(0)}, 113 | {int64(-1), byte(0)}, 114 | {[]byte("junk"), false}, 115 | {redis.Error("blah"), false}, 116 | {redis.Error("blah"), durationScan{Duration: time.Minute}}, 117 | {"invalid", durationScan{Duration: time.Minute}}, 118 | } 119 | 120 | func TestScanConversionError(t *testing.T) { 121 | for _, tt := range scanConversionErrorTests { 122 | values := []interface{}{tt.src} 123 | dest := reflect.New(reflect.TypeOf(tt.dest)) 124 | values, err := redis.Scan(values, dest.Interface()) 125 | if err == nil { 126 | t.Errorf("Scan(%v) did not return error", tt) 127 | } 128 | } 129 | } 130 | 131 | func ExampleScan() { 132 | c, err := dial() 133 | if err != nil { 134 | fmt.Println(err) 135 | return 136 | } 137 | defer c.Close() 138 | 139 | c.Send("HMSET", "album:1", "title", "Red", "rating", 5) 140 | c.Send("HMSET", "album:2", "title", "Earthbound", "rating", 1) 141 | c.Send("HMSET", "album:3", "title", "Beat") 142 | c.Send("LPUSH", "albums", "1") 143 | c.Send("LPUSH", "albums", "2") 144 | c.Send("LPUSH", "albums", "3") 145 | values, err := redis.Values(c.Do("SORT", "albums", 146 | "BY", "album:*->rating", 147 | "GET", "album:*->title", 148 | "GET", "album:*->rating")) 149 | if err != nil { 150 | fmt.Println(err) 151 | return 152 | } 153 | 154 | for len(values) > 0 { 155 | var title string 156 | rating := -1 // initialize to illegal value to detect nil. 157 | values, err = redis.Scan(values, &title, &rating) 158 | if err != nil { 159 | fmt.Println(err) 160 | return 161 | } 162 | if rating == -1 { 163 | fmt.Println(title, "not-rated") 164 | } else { 165 | fmt.Println(title, rating) 166 | } 167 | } 168 | // Output: 169 | // Beat not-rated 170 | // Earthbound 1 171 | // Red 5 172 | } 173 | 174 | type s0 struct { 175 | X int 176 | Y int `redis:"y"` 177 | Bt bool 178 | } 179 | 180 | type s1 struct { 181 | X int `redis:"-"` 182 | I int `redis:"i"` 183 | U uint `redis:"u"` 184 | S string `redis:"s"` 185 | P []byte `redis:"p"` 186 | B bool `redis:"b"` 187 | Bt bool 188 | Bf bool 189 | s0 190 | Sd durationScan `redis:"sd"` 191 | Sdp *durationScan `redis:"sdp"` 192 | } 193 | 194 | var scanStructTests = []struct { 195 | title string 196 | reply []string 197 | value interface{} 198 | }{ 199 | {"basic", 200 | []string{ 201 | "i", "-1234", 202 | "u", "5678", 203 | "s", "hello", 204 | "p", "world", 205 | "b", "t", 206 | "Bt", "1", 207 | "Bf", "0", 208 | "X", "123", 209 | "y", "456", 210 | "sd", "1m", 211 | "sdp", "1m", 212 | }, 213 | &s1{ 214 | I: -1234, 215 | U: 5678, 216 | S: "hello", 217 | P: []byte("world"), 218 | B: true, 219 | Bt: true, 220 | Bf: false, 221 | s0: s0{X: 123, Y: 456}, 222 | Sd: durationScan{Duration: time.Minute}, 223 | Sdp: &durationScan{Duration: time.Minute}, 224 | }, 225 | }, 226 | } 227 | 228 | func TestScanStruct(t *testing.T) { 229 | for _, tt := range scanStructTests { 230 | 231 | var reply []interface{} 232 | for _, v := range tt.reply { 233 | reply = append(reply, []byte(v)) 234 | } 235 | 236 | value := reflect.New(reflect.ValueOf(tt.value).Type().Elem()) 237 | 238 | if err := redis.ScanStruct(reply, value.Interface()); err != nil { 239 | t.Fatalf("ScanStruct(%s) returned error %v", tt.title, err) 240 | } 241 | 242 | if !reflect.DeepEqual(value.Interface(), tt.value) { 243 | t.Fatalf("ScanStruct(%s) returned %v, want %v", tt.title, value.Interface(), tt.value) 244 | } 245 | } 246 | } 247 | 248 | func TestBadScanStructArgs(t *testing.T) { 249 | x := []interface{}{"A", "b"} 250 | test := func(v interface{}) { 251 | if err := redis.ScanStruct(x, v); err == nil { 252 | t.Errorf("Expect error for ScanStruct(%T, %T)", x, v) 253 | } 254 | } 255 | 256 | test(nil) 257 | 258 | var v0 *struct{} 259 | test(v0) 260 | 261 | var v1 int 262 | test(&v1) 263 | 264 | x = x[:1] 265 | v2 := struct{ A string }{} 266 | test(&v2) 267 | } 268 | 269 | var scanSliceTests = []struct { 270 | src []interface{} 271 | fieldNames []string 272 | ok bool 273 | dest interface{} 274 | }{ 275 | { 276 | []interface{}{[]byte("1"), nil, []byte("-1")}, 277 | nil, 278 | true, 279 | []int{1, 0, -1}, 280 | }, 281 | { 282 | []interface{}{[]byte("1"), nil, []byte("2")}, 283 | nil, 284 | true, 285 | []uint{1, 0, 2}, 286 | }, 287 | { 288 | []interface{}{[]byte("-1")}, 289 | nil, 290 | false, 291 | []uint{1}, 292 | }, 293 | { 294 | []interface{}{[]byte("hello"), nil, []byte("world")}, 295 | nil, 296 | true, 297 | [][]byte{[]byte("hello"), nil, []byte("world")}, 298 | }, 299 | { 300 | []interface{}{[]byte("hello"), nil, []byte("world")}, 301 | nil, 302 | true, 303 | []string{"hello", "", "world"}, 304 | }, 305 | { 306 | []interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")}, 307 | nil, 308 | true, 309 | []struct{ A, B string }{{"a1", "b1"}, {"a2", "b2"}}, 310 | }, 311 | { 312 | []interface{}{[]byte("a1"), []byte("b1")}, 313 | nil, 314 | false, 315 | []struct{ A, B, C string }{{"a1", "b1", ""}}, 316 | }, 317 | { 318 | []interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")}, 319 | nil, 320 | true, 321 | []*struct{ A, B string }{{"a1", "b1"}, {"a2", "b2"}}, 322 | }, 323 | { 324 | []interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")}, 325 | []string{"A", "B"}, 326 | true, 327 | []struct{ A, C, B string }{{"a1", "", "b1"}, {"a2", "", "b2"}}, 328 | }, 329 | { 330 | []interface{}{[]byte("a1"), []byte("b1"), []byte("a2"), []byte("b2")}, 331 | nil, 332 | false, 333 | []struct{}{}, 334 | }, 335 | } 336 | 337 | func TestScanSlice(t *testing.T) { 338 | for _, tt := range scanSliceTests { 339 | 340 | typ := reflect.ValueOf(tt.dest).Type() 341 | dest := reflect.New(typ) 342 | 343 | err := redis.ScanSlice(tt.src, dest.Interface(), tt.fieldNames...) 344 | if tt.ok != (err == nil) { 345 | t.Errorf("ScanSlice(%v, []%s, %v) returned error %v", tt.src, typ, tt.fieldNames, err) 346 | continue 347 | } 348 | if tt.ok && !reflect.DeepEqual(dest.Elem().Interface(), tt.dest) { 349 | t.Errorf("ScanSlice(src, []%s) returned %#v, want %#v", typ, dest.Elem().Interface(), tt.dest) 350 | } 351 | } 352 | } 353 | 354 | func ExampleScanSlice() { 355 | c, err := dial() 356 | if err != nil { 357 | fmt.Println(err) 358 | return 359 | } 360 | defer c.Close() 361 | 362 | c.Send("HMSET", "album:1", "title", "Red", "rating", 5) 363 | c.Send("HMSET", "album:2", "title", "Earthbound", "rating", 1) 364 | c.Send("HMSET", "album:3", "title", "Beat", "rating", 4) 365 | c.Send("LPUSH", "albums", "1") 366 | c.Send("LPUSH", "albums", "2") 367 | c.Send("LPUSH", "albums", "3") 368 | values, err := redis.Values(c.Do("SORT", "albums", 369 | "BY", "album:*->rating", 370 | "GET", "album:*->title", 371 | "GET", "album:*->rating")) 372 | if err != nil { 373 | fmt.Println(err) 374 | return 375 | } 376 | 377 | var albums []struct { 378 | Title string 379 | Rating int 380 | } 381 | if err := redis.ScanSlice(values, &albums); err != nil { 382 | fmt.Println(err) 383 | return 384 | } 385 | fmt.Printf("%v\n", albums) 386 | // Output: 387 | // [{Earthbound 1} {Beat 4} {Red 5}] 388 | } 389 | 390 | var argsTests = []struct { 391 | title string 392 | actual redis.Args 393 | expected redis.Args 394 | }{ 395 | {"struct ptr", 396 | redis.Args{}.AddFlat(&struct { 397 | I int `redis:"i"` 398 | U uint `redis:"u"` 399 | S string `redis:"s"` 400 | P []byte `redis:"p"` 401 | M map[string]string `redis:"m"` 402 | Bt bool 403 | Bf bool 404 | }{ 405 | -1234, 5678, "hello", []byte("world"), map[string]string{"hello": "world"}, true, false, 406 | }), 407 | redis.Args{"i", int(-1234), "u", uint(5678), "s", "hello", "p", []byte("world"), "m", map[string]string{"hello": "world"}, "Bt", true, "Bf", false}, 408 | }, 409 | {"struct", 410 | redis.Args{}.AddFlat(struct{ I int }{123}), 411 | redis.Args{"I", 123}, 412 | }, 413 | {"slice", 414 | redis.Args{}.Add(1).AddFlat([]string{"a", "b", "c"}).Add(2), 415 | redis.Args{1, "a", "b", "c", 2}, 416 | }, 417 | {"struct omitempty", 418 | redis.Args{}.AddFlat(&struct { 419 | I int `redis:"i,omitempty"` 420 | U uint `redis:"u,omitempty"` 421 | S string `redis:"s,omitempty"` 422 | P []byte `redis:"p,omitempty"` 423 | M map[string]string `redis:"m,omitempty"` 424 | Bt bool `redis:"Bt,omitempty"` 425 | Bf bool `redis:"Bf,omitempty"` 426 | }{ 427 | 0, 0, "", []byte{}, map[string]string{}, true, false, 428 | }), 429 | redis.Args{"Bt", true}, 430 | }, 431 | } 432 | 433 | func TestArgs(t *testing.T) { 434 | for _, tt := range argsTests { 435 | if !reflect.DeepEqual(tt.actual, tt.expected) { 436 | t.Fatalf("%s is %v, want %v", tt.title, tt.actual, tt.expected) 437 | } 438 | } 439 | } 440 | 441 | func ExampleArgs() { 442 | c, err := dial() 443 | if err != nil { 444 | fmt.Println(err) 445 | return 446 | } 447 | defer c.Close() 448 | 449 | var p1, p2 struct { 450 | Title string `redis:"title"` 451 | Author string `redis:"author"` 452 | Body string `redis:"body"` 453 | } 454 | 455 | p1.Title = "Example" 456 | p1.Author = "Gary" 457 | p1.Body = "Hello" 458 | 459 | if _, err := c.Do("HMSET", redis.Args{}.Add("id1").AddFlat(&p1)...); err != nil { 460 | fmt.Println(err) 461 | return 462 | } 463 | 464 | m := map[string]string{ 465 | "title": "Example2", 466 | "author": "Steve", 467 | "body": "Map", 468 | } 469 | 470 | if _, err := c.Do("HMSET", redis.Args{}.Add("id2").AddFlat(m)...); err != nil { 471 | fmt.Println(err) 472 | return 473 | } 474 | 475 | for _, id := range []string{"id1", "id2"} { 476 | 477 | v, err := redis.Values(c.Do("HGETALL", id)) 478 | if err != nil { 479 | fmt.Println(err) 480 | return 481 | } 482 | 483 | if err := redis.ScanStruct(v, &p2); err != nil { 484 | fmt.Println(err) 485 | return 486 | } 487 | 488 | fmt.Printf("%+v\n", p2) 489 | } 490 | 491 | // Output: 492 | // {Title:Example Author:Gary Body:Hello} 493 | // {Title:Example2 Author:Steve Body:Map} 494 | } 495 | -------------------------------------------------------------------------------- /redis/reply.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Gary Burd 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package redis 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "strconv" 21 | ) 22 | 23 | // ErrNil indicates that a reply value is nil. 24 | var ErrNil = errors.New("RedisGo-Async: nil returned") 25 | 26 | // Int is a helper that converts a command reply to an integer. If err is not 27 | // equal to nil, then Int returns 0, err. Otherwise, Int converts the 28 | // reply to an int as follows: 29 | // 30 | // Reply type Result 31 | // integer int(reply), nil 32 | // bulk string parsed reply, nil 33 | // nil 0, ErrNil 34 | // other 0, error 35 | func Int(reply interface{}, err error) (int, error) { 36 | if err != nil { 37 | return 0, err 38 | } 39 | switch reply := reply.(type) { 40 | case int64: 41 | x := int(reply) 42 | if int64(x) != reply { 43 | return 0, strconv.ErrRange 44 | } 45 | return x, nil 46 | case []byte: 47 | n, err := strconv.ParseInt(string(reply), 10, 0) 48 | return int(n), err 49 | case nil: 50 | return 0, ErrNil 51 | case Error: 52 | return 0, reply 53 | } 54 | return 0, fmt.Errorf("RedisGo-Async: unexpected type for Int, got type %T", reply) 55 | } 56 | 57 | // Int64 is a helper that converts a command reply to 64 bit integer. If err is 58 | // not equal to nil, then Int returns 0, err. Otherwise, Int64 converts the 59 | // reply to an int64 as follows: 60 | // 61 | // Reply type Result 62 | // integer reply, nil 63 | // bulk string parsed reply, nil 64 | // nil 0, ErrNil 65 | // other 0, error 66 | func Int64(reply interface{}, err error) (int64, error) { 67 | if err != nil { 68 | return 0, err 69 | } 70 | switch reply := reply.(type) { 71 | case int64: 72 | return reply, nil 73 | case []byte: 74 | n, err := strconv.ParseInt(string(reply), 10, 64) 75 | return n, err 76 | case nil: 77 | return 0, ErrNil 78 | case Error: 79 | return 0, reply 80 | } 81 | return 0, fmt.Errorf("RedisGo-Async: unexpected type for Int64, got type %T", reply) 82 | } 83 | 84 | var errNegativeInt = errors.New("RedisGo-Async: unexpected value for Uint64") 85 | 86 | // Uint64 is a helper that converts a command reply to 64 bit integer. If err is 87 | // not equal to nil, then Int returns 0, err. Otherwise, Int64 converts the 88 | // reply to an int64 as follows: 89 | // 90 | // Reply type Result 91 | // integer reply, nil 92 | // bulk string parsed reply, nil 93 | // nil 0, ErrNil 94 | // other 0, error 95 | func Uint64(reply interface{}, err error) (uint64, error) { 96 | if err != nil { 97 | return 0, err 98 | } 99 | switch reply := reply.(type) { 100 | case int64: 101 | if reply < 0 { 102 | return 0, errNegativeInt 103 | } 104 | return uint64(reply), nil 105 | case []byte: 106 | n, err := strconv.ParseUint(string(reply), 10, 64) 107 | return n, err 108 | case nil: 109 | return 0, ErrNil 110 | case Error: 111 | return 0, reply 112 | } 113 | return 0, fmt.Errorf("RedisGo-Async: unexpected type for Uint64, got type %T", reply) 114 | } 115 | 116 | // Float64 is a helper that converts a command reply to 64 bit float. If err is 117 | // not equal to nil, then Float64 returns 0, err. Otherwise, Float64 converts 118 | // the reply to an int as follows: 119 | // 120 | // Reply type Result 121 | // bulk string parsed reply, nil 122 | // nil 0, ErrNil 123 | // other 0, error 124 | func Float64(reply interface{}, err error) (float64, error) { 125 | if err != nil { 126 | return 0, err 127 | } 128 | switch reply := reply.(type) { 129 | case []byte: 130 | n, err := strconv.ParseFloat(string(reply), 64) 131 | return n, err 132 | case nil: 133 | return 0, ErrNil 134 | case Error: 135 | return 0, reply 136 | } 137 | return 0, fmt.Errorf("RedisGo-Async: unexpected type for Float64, got type %T", reply) 138 | } 139 | 140 | // String is a helper that converts a command reply to a string. If err is not 141 | // equal to nil, then String returns "", err. Otherwise String converts the 142 | // reply to a string as follows: 143 | // 144 | // Reply type Result 145 | // bulk string string(reply), nil 146 | // simple string reply, nil 147 | // nil "", ErrNil 148 | // other "", error 149 | func String(reply interface{}, err error) (string, error) { 150 | if err != nil { 151 | return "", err 152 | } 153 | switch reply := reply.(type) { 154 | case []byte: 155 | return string(reply), nil 156 | case string: 157 | return reply, nil 158 | case nil: 159 | return "", ErrNil 160 | case Error: 161 | return "", reply 162 | } 163 | return "", fmt.Errorf("RedisGo-Async: unexpected type for String, got type %T", reply) 164 | } 165 | 166 | // Bytes is a helper that converts a command reply to a slice of bytes. If err 167 | // is not equal to nil, then Bytes returns nil, err. Otherwise Bytes converts 168 | // the reply to a slice of bytes as follows: 169 | // 170 | // Reply type Result 171 | // bulk string reply, nil 172 | // simple string []byte(reply), nil 173 | // nil nil, ErrNil 174 | // other nil, error 175 | func Bytes(reply interface{}, err error) ([]byte, error) { 176 | if err != nil { 177 | return nil, err 178 | } 179 | switch reply := reply.(type) { 180 | case []byte: 181 | return reply, nil 182 | case string: 183 | return []byte(reply), nil 184 | case nil: 185 | return nil, ErrNil 186 | case Error: 187 | return nil, reply 188 | } 189 | return nil, fmt.Errorf("RedisGo-Async: unexpected type for Bytes, got type %T", reply) 190 | } 191 | 192 | // Bool is a helper that converts a command reply to a boolean. If err is not 193 | // equal to nil, then Bool returns false, err. Otherwise Bool converts the 194 | // reply to boolean as follows: 195 | // 196 | // Reply type Result 197 | // integer value != 0, nil 198 | // bulk string strconv.ParseBool(reply) 199 | // nil false, ErrNil 200 | // other false, error 201 | func Bool(reply interface{}, err error) (bool, error) { 202 | if err != nil { 203 | return false, err 204 | } 205 | switch reply := reply.(type) { 206 | case int64: 207 | return reply != 0, nil 208 | case []byte: 209 | return strconv.ParseBool(string(reply)) 210 | case nil: 211 | return false, ErrNil 212 | case Error: 213 | return false, reply 214 | } 215 | return false, fmt.Errorf("RedisGo-Async: unexpected type for Bool, got type %T", reply) 216 | } 217 | 218 | // MultiBulk is a helper that converts an array command reply to a []interface{}. 219 | // 220 | // Deprecated: Use Values instead. 221 | func MultiBulk(reply interface{}, err error) ([]interface{}, error) { return Values(reply, err) } 222 | 223 | // Values is a helper that converts an array command reply to a []interface{}. 224 | // If err is not equal to nil, then Values returns nil, err. Otherwise, Values 225 | // converts the reply as follows: 226 | // 227 | // Reply type Result 228 | // array reply, nil 229 | // nil nil, ErrNil 230 | // other nil, error 231 | func Values(reply interface{}, err error) ([]interface{}, error) { 232 | if err != nil { 233 | return nil, err 234 | } 235 | switch reply := reply.(type) { 236 | case []interface{}: 237 | return reply, nil 238 | case nil: 239 | return nil, ErrNil 240 | case Error: 241 | return nil, reply 242 | } 243 | return nil, fmt.Errorf("RedisGo-Async: unexpected type for Values, got type %T", reply) 244 | } 245 | 246 | // Strings is a helper that converts an array command reply to a []string. If 247 | // err is not equal to nil, then Strings returns nil, err. Nil array items are 248 | // converted to "" in the output slice. Strings returns an error if an array 249 | // item is not a bulk string or nil. 250 | func Strings(reply interface{}, err error) ([]string, error) { 251 | if err != nil { 252 | return nil, err 253 | } 254 | switch reply := reply.(type) { 255 | case []interface{}: 256 | result := make([]string, len(reply)) 257 | for i := range reply { 258 | if reply[i] == nil { 259 | continue 260 | } 261 | p, ok := reply[i].([]byte) 262 | if !ok { 263 | return nil, fmt.Errorf("RedisGo-Async: unexpected element type for Strings, got type %T", reply[i]) 264 | } 265 | result[i] = string(p) 266 | } 267 | return result, nil 268 | case nil: 269 | return nil, ErrNil 270 | case Error: 271 | return nil, reply 272 | } 273 | return nil, fmt.Errorf("RedisGo-Async: unexpected type for Strings, got type %T", reply) 274 | } 275 | 276 | // ByteSlices is a helper that converts an array command reply to a [][]byte. 277 | // If err is not equal to nil, then ByteSlices returns nil, err. Nil array 278 | // items are stay nil. ByteSlices returns an error if an array item is not a 279 | // bulk string or nil. 280 | func ByteSlices(reply interface{}, err error) ([][]byte, error) { 281 | if err != nil { 282 | return nil, err 283 | } 284 | switch reply := reply.(type) { 285 | case []interface{}: 286 | result := make([][]byte, len(reply)) 287 | for i := range reply { 288 | if reply[i] == nil { 289 | continue 290 | } 291 | p, ok := reply[i].([]byte) 292 | if !ok { 293 | return nil, fmt.Errorf("RedisGo-Async: unexpected element type for ByteSlices, got type %T", reply[i]) 294 | } 295 | result[i] = p 296 | } 297 | return result, nil 298 | case nil: 299 | return nil, ErrNil 300 | case Error: 301 | return nil, reply 302 | } 303 | return nil, fmt.Errorf("RedisGo-Async: unexpected type for ByteSlices, got type %T", reply) 304 | } 305 | 306 | // Ints is a helper that converts an array command reply to a []int. If 307 | // err is not equal to nil, then Ints returns nil, err. 308 | func Ints(reply interface{}, err error) ([]int, error) { 309 | var ints []int 310 | values, err := Values(reply, err) 311 | if err != nil { 312 | return ints, err 313 | } 314 | if err := ScanSlice(values, &ints); err != nil { 315 | return ints, err 316 | } 317 | return ints, nil 318 | } 319 | 320 | // StringMap is a helper that converts an array of strings (alternating key, value) 321 | // into a map[string]string. The HGETALL and CONFIG GET commands return replies in this format. 322 | // Requires an even number of values in result. 323 | func StringMap(result interface{}, err error) (map[string]string, error) { 324 | values, err := Values(result, err) 325 | if err != nil { 326 | return nil, err 327 | } 328 | if len(values)%2 != 0 { 329 | return nil, errors.New("RedisGo-Async: StringMap expects even number of values result") 330 | } 331 | m := make(map[string]string, len(values)/2) 332 | for i := 0; i < len(values); i += 2 { 333 | key, okKey := values[i].([]byte) 334 | value, okValue := values[i+1].([]byte) 335 | if !okKey || !okValue { 336 | return nil, errors.New("RedisGo-Async: ScanMap key not a bulk string value") 337 | } 338 | m[string(key)] = string(value) 339 | } 340 | return m, nil 341 | } 342 | 343 | // IntMap is a helper that converts an array of strings (alternating key, value) 344 | // into a map[string]int. The HGETALL commands return replies in this format. 345 | // Requires an even number of values in result. 346 | func IntMap(result interface{}, err error) (map[string]int, error) { 347 | values, err := Values(result, err) 348 | if err != nil { 349 | return nil, err 350 | } 351 | if len(values)%2 != 0 { 352 | return nil, errors.New("RedisGo-Async: IntMap expects even number of values result") 353 | } 354 | m := make(map[string]int, len(values)/2) 355 | for i := 0; i < len(values); i += 2 { 356 | key, ok := values[i].([]byte) 357 | if !ok { 358 | return nil, errors.New("RedisGo-Async: ScanMap key not a bulk string value") 359 | } 360 | value, err := Int(values[i+1], nil) 361 | if err != nil { 362 | return nil, err 363 | } 364 | m[string(key)] = value 365 | } 366 | return m, nil 367 | } 368 | 369 | // Int64Map is a helper that converts an array of strings (alternating key, value) 370 | // into a map[string]int64. The HGETALL commands return replies in this format. 371 | // Requires an even number of values in result. 372 | func Int64Map(result interface{}, err error) (map[string]int64, error) { 373 | values, err := Values(result, err) 374 | if err != nil { 375 | return nil, err 376 | } 377 | if len(values)%2 != 0 { 378 | return nil, errors.New("RedisGo-Async: Int64Map expects even number of values result") 379 | } 380 | m := make(map[string]int64, len(values)/2) 381 | for i := 0; i < len(values); i += 2 { 382 | key, ok := values[i].([]byte) 383 | if !ok { 384 | return nil, errors.New("RedisGo-Async: ScanMap key not a bulk string value") 385 | } 386 | value, err := Int64(values[i+1], nil) 387 | if err != nil { 388 | return nil, err 389 | } 390 | m[string(key)] = value 391 | } 392 | return m, nil 393 | } 394 | 395 | // Positions is a helper that converts an array of positions (lat, long) 396 | // into a [][2]float64. The GEOPOS command returns replies in this format. 397 | func Positions(result interface{}, err error) ([]*[2]float64, error) { 398 | values, err := Values(result, err) 399 | if err != nil { 400 | return nil, err 401 | } 402 | positions := make([]*[2]float64, len(values)) 403 | for i := range values { 404 | if values[i] == nil { 405 | continue 406 | } 407 | p, ok := values[i].([]interface{}) 408 | if !ok { 409 | return nil, fmt.Errorf("RedisGo-Async: unexpected element type for interface slice, got type %T", values[i]) 410 | } 411 | if len(p) != 2 { 412 | return nil, fmt.Errorf("RedisGo-Async: unexpected number of values for a member position, got %d", len(p)) 413 | } 414 | lat, err := Float64(p[0], nil) 415 | if err != nil { 416 | return nil, err 417 | } 418 | long, err := Float64(p[1], nil) 419 | if err != nil { 420 | return nil, err 421 | } 422 | positions[i] = &[2]float64{lat, long} 423 | } 424 | return positions, nil 425 | } 426 | -------------------------------------------------------------------------------- /redis/pool_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 Gary Burd 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package redis_test 16 | 17 | import ( 18 | "errors" 19 | "io" 20 | "reflect" 21 | "sync" 22 | "testing" 23 | "time" 24 | 25 | "redis" 26 | ) 27 | 28 | type poolTestConn struct { 29 | d *poolDialer 30 | err error 31 | redis.Conn 32 | } 33 | 34 | func (c *poolTestConn) Close() error { 35 | c.d.mu.Lock() 36 | c.d.open -= 1 37 | c.d.mu.Unlock() 38 | return c.Conn.Close() 39 | } 40 | 41 | func (c *poolTestConn) Err() error { return c.err } 42 | 43 | func (c *poolTestConn) Do(commandName string, args ...interface{}) (interface{}, error) { 44 | if commandName == "ERR" { 45 | c.err = args[0].(error) 46 | commandName = "PING" 47 | } 48 | if commandName != "" { 49 | c.d.commands = append(c.d.commands, commandName) 50 | } 51 | return c.Conn.Do(commandName, args...) 52 | } 53 | 54 | func (c *poolTestConn) Send(commandName string, args ...interface{}) error { 55 | c.d.commands = append(c.d.commands, commandName) 56 | return c.Conn.Send(commandName, args...) 57 | } 58 | 59 | type poolDialer struct { 60 | mu sync.Mutex 61 | t *testing.T 62 | dialed int 63 | open int 64 | commands []string 65 | dialErr error 66 | } 67 | 68 | func (d *poolDialer) dial() (redis.Conn, error) { 69 | d.mu.Lock() 70 | d.dialed += 1 71 | dialErr := d.dialErr 72 | d.mu.Unlock() 73 | if dialErr != nil { 74 | return nil, d.dialErr 75 | } 76 | c, err := redis.DialDefaultServer() 77 | if err != nil { 78 | return nil, err 79 | } 80 | d.mu.Lock() 81 | d.open += 1 82 | d.mu.Unlock() 83 | return &poolTestConn{d: d, Conn: c}, nil 84 | } 85 | 86 | func (d *poolDialer) check(message string, p *redis.Pool, dialed, open, inuse int) { 87 | d.mu.Lock() 88 | if d.dialed != dialed { 89 | d.t.Errorf("%s: dialed=%d, want %d", message, d.dialed, dialed) 90 | } 91 | if d.open != open { 92 | d.t.Errorf("%s: open=%d, want %d", message, d.open, open) 93 | } 94 | 95 | if active := p.ActiveCount(); active != open { 96 | d.t.Errorf("%s: active=%d, want %d", message, active, open) 97 | } 98 | if idle := p.IdleCount(); idle != open-inuse { 99 | d.t.Errorf("%s: idle=%d, want %d", message, idle, open-inuse) 100 | } 101 | d.mu.Unlock() 102 | } 103 | 104 | func TestPoolReuse(t *testing.T) { 105 | d := poolDialer{t: t} 106 | p := &redis.Pool{ 107 | MaxIdle: 2, 108 | Dial: d.dial, 109 | } 110 | 111 | for i := 0; i < 10; i++ { 112 | c1 := p.Get() 113 | c1.Do("PING") 114 | c2 := p.Get() 115 | c2.Do("PING") 116 | c1.Close() 117 | c2.Close() 118 | } 119 | 120 | d.check("before close", p, 2, 2, 0) 121 | p.Close() 122 | d.check("after close", p, 2, 0, 0) 123 | } 124 | 125 | func TestPoolMaxIdle(t *testing.T) { 126 | d := poolDialer{t: t} 127 | p := &redis.Pool{ 128 | MaxIdle: 2, 129 | Dial: d.dial, 130 | } 131 | defer p.Close() 132 | 133 | for i := 0; i < 10; i++ { 134 | c1 := p.Get() 135 | c1.Do("PING") 136 | c2 := p.Get() 137 | c2.Do("PING") 138 | c3 := p.Get() 139 | c3.Do("PING") 140 | c1.Close() 141 | c2.Close() 142 | c3.Close() 143 | } 144 | d.check("before close", p, 12, 2, 0) 145 | p.Close() 146 | d.check("after close", p, 12, 0, 0) 147 | } 148 | 149 | func TestPoolError(t *testing.T) { 150 | d := poolDialer{t: t} 151 | p := &redis.Pool{ 152 | MaxIdle: 2, 153 | Dial: d.dial, 154 | } 155 | defer p.Close() 156 | 157 | c := p.Get() 158 | c.Do("ERR", io.EOF) 159 | if c.Err() == nil { 160 | t.Errorf("expected c.Err() != nil") 161 | } 162 | c.Close() 163 | 164 | c = p.Get() 165 | c.Do("ERR", io.EOF) 166 | c.Close() 167 | 168 | d.check(".", p, 2, 0, 0) 169 | } 170 | 171 | func TestPoolClose(t *testing.T) { 172 | d := poolDialer{t: t} 173 | p := &redis.Pool{ 174 | MaxIdle: 2, 175 | Dial: d.dial, 176 | } 177 | defer p.Close() 178 | 179 | c1 := p.Get() 180 | c1.Do("PING") 181 | c2 := p.Get() 182 | c2.Do("PING") 183 | c3 := p.Get() 184 | c3.Do("PING") 185 | 186 | c1.Close() 187 | if _, err := c1.Do("PING"); err == nil { 188 | t.Errorf("expected error after connection closed") 189 | } 190 | 191 | c2.Close() 192 | c2.Close() 193 | 194 | p.Close() 195 | 196 | d.check("after pool close", p, 3, 1, 1) 197 | 198 | if _, err := c1.Do("PING"); err == nil { 199 | t.Errorf("expected error after connection and pool closed") 200 | } 201 | 202 | c3.Close() 203 | 204 | d.check("after conn close", p, 3, 0, 0) 205 | 206 | c1 = p.Get() 207 | if _, err := c1.Do("PING"); err == nil { 208 | t.Errorf("expected error after pool closed") 209 | } 210 | } 211 | 212 | func TestPoolTimeout(t *testing.T) { 213 | d := poolDialer{t: t} 214 | p := &redis.Pool{ 215 | MaxIdle: 2, 216 | IdleTimeout: 300 * time.Second, 217 | Dial: d.dial, 218 | } 219 | defer p.Close() 220 | 221 | now := time.Now() 222 | redis.SetNowFunc(func() time.Time { return now }) 223 | defer redis.SetNowFunc(time.Now) 224 | 225 | c := p.Get() 226 | c.Do("PING") 227 | c.Close() 228 | 229 | d.check("1", p, 1, 1, 0) 230 | 231 | now = now.Add(p.IdleTimeout) 232 | 233 | c = p.Get() 234 | c.Do("PING") 235 | c.Close() 236 | 237 | d.check("2", p, 2, 1, 0) 238 | } 239 | 240 | func TestPoolConcurrenSendReceive(t *testing.T) { 241 | p := &redis.Pool{ 242 | Dial: redis.DialDefaultServer, 243 | } 244 | defer p.Close() 245 | 246 | c := p.Get() 247 | done := make(chan error, 1) 248 | go func() { 249 | _, err := c.Receive() 250 | done <- err 251 | }() 252 | c.Send("PING") 253 | c.Flush() 254 | err := <-done 255 | if err != nil { 256 | t.Fatalf("Receive() returned error %v", err) 257 | } 258 | _, err = c.Do("") 259 | if err != nil { 260 | t.Fatalf("Do() returned error %v", err) 261 | } 262 | c.Close() 263 | } 264 | 265 | func TestPoolBorrowCheck(t *testing.T) { 266 | d := poolDialer{t: t} 267 | p := &redis.Pool{ 268 | MaxIdle: 2, 269 | Dial: d.dial, 270 | TestOnBorrow: func(redis.Conn, time.Time) error { return redis.Error("BLAH") }, 271 | } 272 | defer p.Close() 273 | 274 | for i := 0; i < 10; i++ { 275 | c := p.Get() 276 | c.Do("PING") 277 | c.Close() 278 | } 279 | d.check("1", p, 10, 1, 0) 280 | } 281 | 282 | func TestPoolMaxActive(t *testing.T) { 283 | d := poolDialer{t: t} 284 | p := &redis.Pool{ 285 | MaxIdle: 2, 286 | MaxActive: 2, 287 | Dial: d.dial, 288 | } 289 | defer p.Close() 290 | 291 | c1 := p.Get() 292 | c1.Do("PING") 293 | c2 := p.Get() 294 | c2.Do("PING") 295 | 296 | d.check("1", p, 2, 2, 2) 297 | 298 | c3 := p.Get() 299 | if _, err := c3.Do("PING"); err != redis.ErrPoolExhausted { 300 | t.Errorf("expected pool exhausted") 301 | } 302 | 303 | c3.Close() 304 | d.check("2", p, 2, 2, 2) 305 | c2.Close() 306 | d.check("3", p, 2, 2, 1) 307 | 308 | c3 = p.Get() 309 | if _, err := c3.Do("PING"); err != nil { 310 | t.Errorf("expected good channel, err=%v", err) 311 | } 312 | c3.Close() 313 | 314 | d.check("4", p, 2, 2, 1) 315 | } 316 | 317 | func TestPoolMonitorCleanup(t *testing.T) { 318 | d := poolDialer{t: t} 319 | p := &redis.Pool{ 320 | MaxIdle: 2, 321 | MaxActive: 2, 322 | Dial: d.dial, 323 | } 324 | defer p.Close() 325 | 326 | c := p.Get() 327 | c.Send("MONITOR") 328 | c.Close() 329 | 330 | d.check("", p, 1, 0, 0) 331 | } 332 | 333 | func TestPoolPubSubCleanup(t *testing.T) { 334 | d := poolDialer{t: t} 335 | p := &redis.Pool{ 336 | MaxIdle: 2, 337 | MaxActive: 2, 338 | Dial: d.dial, 339 | } 340 | defer p.Close() 341 | 342 | c := p.Get() 343 | c.Send("SUBSCRIBE", "x") 344 | c.Close() 345 | 346 | want := []string{"SUBSCRIBE", "UNSUBSCRIBE", "PUNSUBSCRIBE", "ECHO"} 347 | if !reflect.DeepEqual(d.commands, want) { 348 | t.Errorf("got commands %v, want %v", d.commands, want) 349 | } 350 | d.commands = nil 351 | 352 | c = p.Get() 353 | c.Send("PSUBSCRIBE", "x*") 354 | c.Close() 355 | 356 | want = []string{"PSUBSCRIBE", "UNSUBSCRIBE", "PUNSUBSCRIBE", "ECHO"} 357 | if !reflect.DeepEqual(d.commands, want) { 358 | t.Errorf("got commands %v, want %v", d.commands, want) 359 | } 360 | d.commands = nil 361 | } 362 | 363 | func TestPoolTransactionCleanup(t *testing.T) { 364 | d := poolDialer{t: t} 365 | p := &redis.Pool{ 366 | MaxIdle: 2, 367 | MaxActive: 2, 368 | Dial: d.dial, 369 | } 370 | defer p.Close() 371 | 372 | c := p.Get() 373 | c.Do("WATCH", "key") 374 | c.Do("PING") 375 | c.Close() 376 | 377 | want := []string{"WATCH", "PING", "UNWATCH"} 378 | if !reflect.DeepEqual(d.commands, want) { 379 | t.Errorf("got commands %v, want %v", d.commands, want) 380 | } 381 | d.commands = nil 382 | 383 | c = p.Get() 384 | c.Do("WATCH", "key") 385 | c.Do("UNWATCH") 386 | c.Do("PING") 387 | c.Close() 388 | 389 | want = []string{"WATCH", "UNWATCH", "PING"} 390 | if !reflect.DeepEqual(d.commands, want) { 391 | t.Errorf("got commands %v, want %v", d.commands, want) 392 | } 393 | d.commands = nil 394 | 395 | c = p.Get() 396 | c.Do("WATCH", "key") 397 | c.Do("MULTI") 398 | c.Do("PING") 399 | c.Close() 400 | 401 | want = []string{"WATCH", "MULTI", "PING", "DISCARD"} 402 | if !reflect.DeepEqual(d.commands, want) { 403 | t.Errorf("got commands %v, want %v", d.commands, want) 404 | } 405 | d.commands = nil 406 | 407 | c = p.Get() 408 | c.Do("WATCH", "key") 409 | c.Do("MULTI") 410 | c.Do("DISCARD") 411 | c.Do("PING") 412 | c.Close() 413 | 414 | want = []string{"WATCH", "MULTI", "DISCARD", "PING"} 415 | if !reflect.DeepEqual(d.commands, want) { 416 | t.Errorf("got commands %v, want %v", d.commands, want) 417 | } 418 | d.commands = nil 419 | 420 | c = p.Get() 421 | c.Do("WATCH", "key") 422 | c.Do("MULTI") 423 | c.Do("EXEC") 424 | c.Do("PING") 425 | c.Close() 426 | 427 | want = []string{"WATCH", "MULTI", "EXEC", "PING"} 428 | if !reflect.DeepEqual(d.commands, want) { 429 | t.Errorf("got commands %v, want %v", d.commands, want) 430 | } 431 | d.commands = nil 432 | } 433 | 434 | func startGoroutines(p *redis.Pool, cmd string, args ...interface{}) chan error { 435 | errs := make(chan error, 10) 436 | for i := 0; i < cap(errs); i++ { 437 | go func() { 438 | c := p.Get() 439 | _, err := c.Do(cmd, args...) 440 | c.Close() 441 | errs <- err 442 | }() 443 | } 444 | 445 | // Wait for goroutines to block. 446 | time.Sleep(time.Second / 4) 447 | 448 | return errs 449 | } 450 | 451 | func TestWaitPool(t *testing.T) { 452 | d := poolDialer{t: t} 453 | p := &redis.Pool{ 454 | MaxIdle: 1, 455 | MaxActive: 1, 456 | Dial: d.dial, 457 | Wait: true, 458 | } 459 | defer p.Close() 460 | 461 | c := p.Get() 462 | errs := startGoroutines(p, "PING") 463 | d.check("before close", p, 1, 1, 1) 464 | c.Close() 465 | timeout := time.After(2 * time.Second) 466 | for i := 0; i < cap(errs); i++ { 467 | select { 468 | case err := <-errs: 469 | if err != nil { 470 | t.Fatal(err) 471 | } 472 | case <-timeout: 473 | t.Fatalf("timeout waiting for blocked goroutine %d", i) 474 | } 475 | } 476 | d.check("done", p, 1, 1, 0) 477 | } 478 | 479 | func TestWaitPoolClose(t *testing.T) { 480 | d := poolDialer{t: t} 481 | p := &redis.Pool{ 482 | MaxIdle: 1, 483 | MaxActive: 1, 484 | Dial: d.dial, 485 | Wait: true, 486 | } 487 | defer p.Close() 488 | 489 | c := p.Get() 490 | if _, err := c.Do("PING"); err != nil { 491 | t.Fatal(err) 492 | } 493 | errs := startGoroutines(p, "PING") 494 | d.check("before close", p, 1, 1, 1) 495 | p.Close() 496 | timeout := time.After(2 * time.Second) 497 | for i := 0; i < cap(errs); i++ { 498 | select { 499 | case err := <-errs: 500 | switch err { 501 | case nil: 502 | t.Fatal("blocked goroutine did not get error") 503 | case redis.ErrPoolExhausted: 504 | t.Fatal("blocked goroutine got pool exhausted error") 505 | } 506 | case <-timeout: 507 | t.Fatal("timeout waiting for blocked goroutine") 508 | } 509 | } 510 | c.Close() 511 | d.check("done", p, 1, 0, 0) 512 | } 513 | 514 | func TestWaitPoolCommandError(t *testing.T) { 515 | testErr := errors.New("test") 516 | d := poolDialer{t: t} 517 | p := &redis.Pool{ 518 | MaxIdle: 1, 519 | MaxActive: 1, 520 | Dial: d.dial, 521 | Wait: true, 522 | } 523 | defer p.Close() 524 | 525 | c := p.Get() 526 | errs := startGoroutines(p, "ERR", testErr) 527 | d.check("before close", p, 1, 1, 1) 528 | c.Close() 529 | timeout := time.After(2 * time.Second) 530 | for i := 0; i < cap(errs); i++ { 531 | select { 532 | case err := <-errs: 533 | if err != nil { 534 | t.Fatal(err) 535 | } 536 | case <-timeout: 537 | t.Fatalf("timeout waiting for blocked goroutine %d", i) 538 | } 539 | } 540 | d.check("done", p, cap(errs), 0, 0) 541 | } 542 | 543 | func TestWaitPoolDialError(t *testing.T) { 544 | testErr := errors.New("test") 545 | d := poolDialer{t: t} 546 | p := &redis.Pool{ 547 | MaxIdle: 1, 548 | MaxActive: 1, 549 | Dial: d.dial, 550 | Wait: true, 551 | } 552 | defer p.Close() 553 | 554 | c := p.Get() 555 | errs := startGoroutines(p, "ERR", testErr) 556 | d.check("before close", p, 1, 1, 1) 557 | 558 | d.dialErr = errors.New("dial") 559 | c.Close() 560 | 561 | nilCount := 0 562 | errCount := 0 563 | timeout := time.After(2 * time.Second) 564 | for i := 0; i < cap(errs); i++ { 565 | select { 566 | case err := <-errs: 567 | switch err { 568 | case nil: 569 | nilCount++ 570 | case d.dialErr: 571 | errCount++ 572 | default: 573 | t.Fatalf("expected dial error or nil, got %v", err) 574 | } 575 | case <-timeout: 576 | t.Fatalf("timeout waiting for blocked goroutine %d", i) 577 | } 578 | } 579 | if nilCount != 1 { 580 | t.Errorf("expected one nil error, got %d", nilCount) 581 | } 582 | if errCount != cap(errs)-1 { 583 | t.Errorf("expected %d dial errors, got %d", cap(errs)-1, errCount) 584 | } 585 | d.check("done", p, cap(errs), 0, 0) 586 | } 587 | 588 | // Borrowing requires us to iterate over the idle connections, unlock the pool, 589 | // and perform a blocking operation to check the connection still works. If 590 | // TestOnBorrow fails, we must reacquire the lock and continue iteration. This 591 | // test ensures that iteration will work correctly if multiple threads are 592 | // iterating simultaneously. 593 | func TestLocking_TestOnBorrowFails_PoolDoesntCrash(t *testing.T) { 594 | const count = 100 595 | 596 | // First we'll Create a pool where the pilfering of idle connections fails. 597 | d := poolDialer{t: t} 598 | p := &redis.Pool{ 599 | MaxIdle: count, 600 | MaxActive: count, 601 | Dial: d.dial, 602 | TestOnBorrow: func(c redis.Conn, t time.Time) error { 603 | return errors.New("No way back into the real world.") 604 | }, 605 | } 606 | defer p.Close() 607 | 608 | // Fill the pool with idle connections. 609 | conns := make([]redis.Conn, count) 610 | for i := range conns { 611 | conns[i] = p.Get() 612 | } 613 | for i := range conns { 614 | conns[i].Close() 615 | } 616 | 617 | // Spawn a bunch of goroutines to thrash the pool. 618 | var wg sync.WaitGroup 619 | wg.Add(count) 620 | for i := 0; i < count; i++ { 621 | go func() { 622 | c := p.Get() 623 | if c.Err() != nil { 624 | t.Errorf("pool get failed: %v", c.Err()) 625 | } 626 | c.Close() 627 | wg.Done() 628 | }() 629 | } 630 | wg.Wait() 631 | if d.dialed != count*2 { 632 | t.Errorf("Expected %d dials, got %d", count*2, d.dialed) 633 | } 634 | } 635 | 636 | func BenchmarkPoolGet(b *testing.B) { 637 | b.StopTimer() 638 | p := redis.Pool{Dial: redis.DialDefaultServer, MaxIdle: 2} 639 | c := p.Get() 640 | if err := c.Err(); err != nil { 641 | b.Fatal(err) 642 | } 643 | c.Close() 644 | defer p.Close() 645 | b.StartTimer() 646 | for i := 0; i < b.N; i++ { 647 | c = p.Get() 648 | c.Close() 649 | } 650 | } 651 | 652 | func BenchmarkPoolGetErr(b *testing.B) { 653 | b.StopTimer() 654 | p := redis.Pool{Dial: redis.DialDefaultServer, MaxIdle: 2} 655 | c := p.Get() 656 | if err := c.Err(); err != nil { 657 | b.Fatal(err) 658 | } 659 | c.Close() 660 | defer p.Close() 661 | b.StartTimer() 662 | for i := 0; i < b.N; i++ { 663 | c = p.Get() 664 | if err := c.Err(); err != nil { 665 | b.Fatal(err) 666 | } 667 | c.Close() 668 | } 669 | } 670 | 671 | func BenchmarkPoolGetPing(b *testing.B) { 672 | b.StopTimer() 673 | p := redis.Pool{Dial: redis.DialDefaultServer, MaxIdle: 2} 674 | c := p.Get() 675 | if err := c.Err(); err != nil { 676 | b.Fatal(err) 677 | } 678 | c.Close() 679 | defer p.Close() 680 | b.StartTimer() 681 | for i := 0; i < b.N; i++ { 682 | c = p.Get() 683 | if _, err := c.Do("PING"); err != nil { 684 | b.Fatal(err) 685 | } 686 | c.Close() 687 | } 688 | } 689 | -------------------------------------------------------------------------------- /redis/conn.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Gary Burd 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package redis 16 | 17 | import ( 18 | "bufio" 19 | "bytes" 20 | "crypto/tls" 21 | "errors" 22 | "fmt" 23 | "io" 24 | "net" 25 | "net/url" 26 | "regexp" 27 | "strconv" 28 | "sync" 29 | "time" 30 | ) 31 | 32 | // conn is the low-level implementation of Conn 33 | type conn struct { 34 | 35 | // Shared 36 | mu sync.Mutex 37 | pending int 38 | err error 39 | conn net.Conn 40 | 41 | // Read 42 | readTimeout time.Duration 43 | br *bufio.Reader 44 | 45 | // Write 46 | writeTimeout time.Duration 47 | bw *bufio.Writer 48 | 49 | // Scratch space for formatting argument length. 50 | // '*' or '$', length, "\r\n" 51 | lenScratch [32]byte 52 | 53 | // Scratch space for formatting integers and floats. 54 | numScratch [40]byte 55 | } 56 | 57 | // DialTimeout acts like Dial but takes timeouts for establishing the 58 | // connection to the server, writing a command and reading a reply. 59 | // 60 | // Deprecated: Use Dial with options instead. 61 | func DialTimeout(network, address string, connectTimeout, readTimeout, writeTimeout time.Duration) (Conn, error) { 62 | return Dial(network, address, 63 | DialConnectTimeout(connectTimeout), 64 | DialReadTimeout(readTimeout), 65 | DialWriteTimeout(writeTimeout)) 66 | } 67 | 68 | // DialOption specifies an option for dialing a Redis server. 69 | type DialOption struct { 70 | f func(*dialOptions) 71 | } 72 | 73 | type dialOptions struct { 74 | readTimeout time.Duration 75 | writeTimeout time.Duration 76 | dial func(network, addr string) (net.Conn, error) 77 | db int 78 | password string 79 | dialTLS bool 80 | skipVerify bool 81 | tlsConfig *tls.Config 82 | } 83 | 84 | // DialReadTimeout specifies the timeout for reading a single command reply. 85 | func DialReadTimeout(d time.Duration) DialOption { 86 | return DialOption{func(do *dialOptions) { 87 | do.readTimeout = d 88 | }} 89 | } 90 | 91 | // DialWriteTimeout specifies the timeout for writing a single command. 92 | func DialWriteTimeout(d time.Duration) DialOption { 93 | return DialOption{func(do *dialOptions) { 94 | do.writeTimeout = d 95 | }} 96 | } 97 | 98 | // DialConnectTimeout specifies the timeout for connecting to the Redis server. 99 | func DialConnectTimeout(d time.Duration) DialOption { 100 | return DialOption{func(do *dialOptions) { 101 | dialer := net.Dialer{Timeout: d} 102 | do.dial = dialer.Dial 103 | }} 104 | } 105 | 106 | // DialNetDial specifies a custom dial function for creating TCP 107 | // connections. If this option is left out, then net.Dial is 108 | // used. DialNetDial overrides DialConnectTimeout. 109 | func DialNetDial(dial func(network, addr string) (net.Conn, error)) DialOption { 110 | return DialOption{func(do *dialOptions) { 111 | do.dial = dial 112 | }} 113 | } 114 | 115 | // DialDatabase specifies the database to select when dialing a connection. 116 | func DialDatabase(db int) DialOption { 117 | return DialOption{func(do *dialOptions) { 118 | do.db = db 119 | }} 120 | } 121 | 122 | // DialPassword specifies the password to use when connecting to 123 | // the Redis server. 124 | func DialPassword(password string) DialOption { 125 | return DialOption{func(do *dialOptions) { 126 | do.password = password 127 | }} 128 | } 129 | 130 | // DialTLSConfig specifies the config to use when a TLS connection is dialed. 131 | // Has no effect when not dialing a TLS connection. 132 | func DialTLSConfig(c *tls.Config) DialOption { 133 | return DialOption{func(do *dialOptions) { 134 | do.tlsConfig = c 135 | }} 136 | } 137 | 138 | // DialTLSSkipVerify to disable server name verification when connecting 139 | // over TLS. Has no effect when not dialing a TLS connection. 140 | func DialTLSSkipVerify(skip bool) DialOption { 141 | return DialOption{func(do *dialOptions) { 142 | do.skipVerify = skip 143 | }} 144 | } 145 | 146 | // Dial connects to the Redis server at the given network and 147 | // address using the specified options. 148 | func Dial(network, address string, options ...DialOption) (Conn, error) { 149 | do := dialOptions{ 150 | dial: net.Dial, 151 | } 152 | for _, option := range options { 153 | option.f(&do) 154 | } 155 | 156 | netConn, err := do.dial(network, address) 157 | if err != nil { 158 | return nil, err 159 | } 160 | 161 | if do.dialTLS { 162 | tlsConfig := cloneTLSClientConfig(do.tlsConfig, do.skipVerify) 163 | if tlsConfig.ServerName == "" { 164 | host, _, err := net.SplitHostPort(address) 165 | if err != nil { 166 | netConn.Close() 167 | return nil, err 168 | } 169 | tlsConfig.ServerName = host 170 | } 171 | 172 | tlsConn := tls.Client(netConn, tlsConfig) 173 | if err := tlsConn.Handshake(); err != nil { 174 | netConn.Close() 175 | return nil, err 176 | } 177 | netConn = tlsConn 178 | } 179 | 180 | c := &conn{ 181 | conn: netConn, 182 | bw: bufio.NewWriter(netConn), 183 | br: bufio.NewReader(netConn), 184 | readTimeout: do.readTimeout, 185 | writeTimeout: do.writeTimeout, 186 | } 187 | 188 | if do.password != "" { 189 | if _, err := c.Do("AUTH", do.password); err != nil { 190 | netConn.Close() 191 | return nil, err 192 | } 193 | } 194 | 195 | if do.db != 0 { 196 | if _, err := c.Do("SELECT", do.db); err != nil { 197 | netConn.Close() 198 | return nil, err 199 | } 200 | } 201 | 202 | return c, nil 203 | } 204 | 205 | func dialTLS(do *dialOptions) { 206 | do.dialTLS = true 207 | } 208 | 209 | var pathDBRegexp = regexp.MustCompile(`/(\d*)\z`) 210 | 211 | // DialURL connects to a Redis server at the given URL using the Redis 212 | // URI scheme. URLs should follow the draft IANA specification for the 213 | // scheme (https://www.iana.org/assignments/uri-schemes/prov/redis). 214 | func DialURL(rawurl string, options ...DialOption) (Conn, error) { 215 | u, err := url.Parse(rawurl) 216 | if err != nil { 217 | return nil, err 218 | } 219 | 220 | if u.Scheme != "redis" && u.Scheme != "rediss" { 221 | return nil, fmt.Errorf("invalid redis URL scheme: %s", u.Scheme) 222 | } 223 | 224 | // As per the IANA draft spec, the host defaults to localhost and 225 | // the port defaults to 6379. 226 | host, port, err := net.SplitHostPort(u.Host) 227 | if err != nil { 228 | // assume port is missing 229 | host = u.Host 230 | port = "6379" 231 | } 232 | if host == "" { 233 | host = "localhost" 234 | } 235 | address := net.JoinHostPort(host, port) 236 | 237 | if u.User != nil { 238 | password, isSet := u.User.Password() 239 | if isSet { 240 | options = append(options, DialPassword(password)) 241 | } 242 | } 243 | 244 | match := pathDBRegexp.FindStringSubmatch(u.Path) 245 | if len(match) == 2 { 246 | db := 0 247 | if len(match[1]) > 0 { 248 | db, err = strconv.Atoi(match[1]) 249 | if err != nil { 250 | return nil, fmt.Errorf("invalid database: %s", u.Path[1:]) 251 | } 252 | } 253 | if db != 0 { 254 | options = append(options, DialDatabase(db)) 255 | } 256 | } else if u.Path != "" { 257 | return nil, fmt.Errorf("invalid database: %s", u.Path[1:]) 258 | } 259 | 260 | if u.Scheme == "rediss" { 261 | options = append([]DialOption{{dialTLS}}, options...) 262 | } 263 | 264 | return Dial("tcp", address, options...) 265 | } 266 | 267 | // NewConn returns a new Redigo connection for the given net connection. 268 | func NewConn(netConn net.Conn, readTimeout, writeTimeout time.Duration) Conn { 269 | return &conn{ 270 | conn: netConn, 271 | bw: bufio.NewWriter(netConn), 272 | br: bufio.NewReader(netConn), 273 | readTimeout: readTimeout, 274 | writeTimeout: writeTimeout, 275 | } 276 | } 277 | 278 | func (c *conn) Close() error { 279 | c.mu.Lock() 280 | err := c.err 281 | if c.err == nil { 282 | c.err = errors.New("RedisGo-Async: closed") 283 | err = c.conn.Close() 284 | } 285 | c.mu.Unlock() 286 | return err 287 | } 288 | 289 | func (c *conn) fatal(err error) error { 290 | c.mu.Lock() 291 | if c.err == nil { 292 | c.err = err 293 | // Close connection to force errors on subsequent calls and to unblock 294 | // other reader or writer. 295 | c.conn.Close() 296 | } 297 | c.mu.Unlock() 298 | return err 299 | } 300 | 301 | func (c *conn) Err() error { 302 | c.mu.Lock() 303 | err := c.err 304 | c.mu.Unlock() 305 | return err 306 | } 307 | 308 | func (c *conn) writeLen(prefix byte, n int) error { 309 | c.lenScratch[len(c.lenScratch)-1] = '\n' 310 | c.lenScratch[len(c.lenScratch)-2] = '\r' 311 | i := len(c.lenScratch) - 3 312 | for { 313 | c.lenScratch[i] = byte('0' + n%10) 314 | i -= 1 315 | n = n / 10 316 | if n == 0 { 317 | break 318 | } 319 | } 320 | c.lenScratch[i] = prefix 321 | _, err := c.bw.Write(c.lenScratch[i:]) 322 | return err 323 | } 324 | 325 | func (c *conn) writeString(s string) error { 326 | c.writeLen('$', len(s)) 327 | c.bw.WriteString(s) 328 | _, err := c.bw.WriteString("\r\n") 329 | return err 330 | } 331 | 332 | func (c *conn) writeBytes(p []byte) error { 333 | c.writeLen('$', len(p)) 334 | c.bw.Write(p) 335 | _, err := c.bw.WriteString("\r\n") 336 | return err 337 | } 338 | 339 | func (c *conn) writeInt64(n int64) error { 340 | return c.writeBytes(strconv.AppendInt(c.numScratch[:0], n, 10)) 341 | } 342 | 343 | func (c *conn) writeFloat64(n float64) error { 344 | return c.writeBytes(strconv.AppendFloat(c.numScratch[:0], n, 'g', -1, 64)) 345 | } 346 | 347 | func (c *conn) writeCommand(cmd string, args []interface{}) (err error) { 348 | c.writeLen('*', 1+len(args)) 349 | err = c.writeString(cmd) 350 | for _, arg := range args { 351 | if err != nil { 352 | break 353 | } 354 | switch arg := arg.(type) { 355 | case string: 356 | err = c.writeString(arg) 357 | case []byte: 358 | err = c.writeBytes(arg) 359 | case int: 360 | err = c.writeInt64(int64(arg)) 361 | case int64: 362 | err = c.writeInt64(arg) 363 | case float64: 364 | err = c.writeFloat64(arg) 365 | case bool: 366 | if arg { 367 | err = c.writeString("1") 368 | } else { 369 | err = c.writeString("0") 370 | } 371 | case nil: 372 | err = c.writeString("") 373 | case Argument: 374 | var buf bytes.Buffer 375 | fmt.Fprint(&buf, arg.RedisArg()) 376 | err = c.writeBytes(buf.Bytes()) 377 | default: 378 | var buf bytes.Buffer 379 | fmt.Fprint(&buf, arg) 380 | err = c.writeBytes(buf.Bytes()) 381 | } 382 | } 383 | return err 384 | } 385 | 386 | type protocolError string 387 | 388 | func (pe protocolError) Error() string { 389 | return fmt.Sprintf("RedisGo-Async: %s (possible server error or unsupported concurrent read by application)", string(pe)) 390 | } 391 | 392 | func (c *conn) readLine() ([]byte, error) { 393 | p, err := c.br.ReadSlice('\n') 394 | if err == bufio.ErrBufferFull { 395 | return nil, protocolError("long response line") 396 | } 397 | if err != nil { 398 | return nil, err 399 | } 400 | i := len(p) - 2 401 | if i < 0 || p[i] != '\r' { 402 | return nil, protocolError("bad response line terminator") 403 | } 404 | return p[:i], nil 405 | } 406 | 407 | // parseLen parses bulk string and array lengths. 408 | func parseLen(p []byte) (int, error) { 409 | if len(p) == 0 { 410 | return -1, protocolError("malformed length") 411 | } 412 | 413 | if p[0] == '-' && len(p) == 2 && p[1] == '1' { 414 | // handle $-1 and $-1 null replies. 415 | return -1, nil 416 | } 417 | 418 | var n int 419 | for _, b := range p { 420 | n *= 10 421 | if b < '0' || b > '9' { 422 | return -1, protocolError("illegal bytes in length") 423 | } 424 | n += int(b - '0') 425 | } 426 | 427 | return n, nil 428 | } 429 | 430 | // parseInt parses an integer reply. 431 | func parseInt(p []byte) (interface{}, error) { 432 | if len(p) == 0 { 433 | return 0, protocolError("malformed integer") 434 | } 435 | 436 | var negate bool 437 | if p[0] == '-' { 438 | negate = true 439 | p = p[1:] 440 | if len(p) == 0 { 441 | return 0, protocolError("malformed integer") 442 | } 443 | } 444 | 445 | var n int64 446 | for _, b := range p { 447 | n *= 10 448 | if b < '0' || b > '9' { 449 | return 0, protocolError("illegal bytes in length") 450 | } 451 | n += int64(b - '0') 452 | } 453 | 454 | if negate { 455 | n = -n 456 | } 457 | return n, nil 458 | } 459 | 460 | var ( 461 | okReply interface{} = "OK" 462 | pongReply interface{} = "PONG" 463 | ) 464 | 465 | func (c *conn) readReply() (interface{}, error) { 466 | line, err := c.readLine() 467 | if err != nil { 468 | return nil, err 469 | } 470 | if len(line) == 0 { 471 | return nil, protocolError("short response line") 472 | } 473 | switch line[0] { 474 | case '+': 475 | switch { 476 | case len(line) == 3 && line[1] == 'O' && line[2] == 'K': 477 | // Avoid allocation for frequent "+OK" response. 478 | return okReply, nil 479 | case len(line) == 5 && line[1] == 'P' && line[2] == 'O' && line[3] == 'N' && line[4] == 'G': 480 | // Avoid allocation in PING command benchmarks :) 481 | return pongReply, nil 482 | default: 483 | return string(line[1:]), nil 484 | } 485 | case '-': 486 | return Error(string(line[1:])), nil 487 | case ':': 488 | return parseInt(line[1:]) 489 | case '$': 490 | n, err := parseLen(line[1:]) 491 | if n < 0 || err != nil { 492 | return nil, err 493 | } 494 | p := make([]byte, n) 495 | _, err = io.ReadFull(c.br, p) 496 | if err != nil { 497 | return nil, err 498 | } 499 | if line, err := c.readLine(); err != nil { 500 | return nil, err 501 | } else if len(line) != 0 { 502 | return nil, protocolError("bad bulk string format") 503 | } 504 | return p, nil 505 | case '*': 506 | n, err := parseLen(line[1:]) 507 | if n < 0 || err != nil { 508 | return nil, err 509 | } 510 | r := make([]interface{}, n) 511 | for i := range r { 512 | r[i], err = c.readReply() 513 | if err != nil { 514 | return nil, err 515 | } 516 | } 517 | return r, nil 518 | } 519 | return nil, protocolError("unexpected response line") 520 | } 521 | 522 | func (c *conn) Send(cmd string, args ...interface{}) error { 523 | c.mu.Lock() 524 | c.pending += 1 525 | c.mu.Unlock() 526 | if c.writeTimeout != 0 { 527 | c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout)) 528 | } 529 | if err := c.writeCommand(cmd, args); err != nil { 530 | return c.fatal(err) 531 | } 532 | return nil 533 | } 534 | 535 | func (c *conn) Flush() error { 536 | if c.writeTimeout != 0 { 537 | c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout)) 538 | } 539 | if err := c.bw.Flush(); err != nil { 540 | return c.fatal(err) 541 | } 542 | return nil 543 | } 544 | 545 | func (c *conn) Receive() (reply interface{}, err error) { 546 | if c.readTimeout != 0 { 547 | c.conn.SetReadDeadline(time.Now().Add(c.readTimeout)) 548 | } 549 | if reply, err = c.readReply(); err != nil { 550 | return nil, c.fatal(err) 551 | } 552 | // When using pub/sub, the number of receives can be greater than the 553 | // number of sends. To enable normal use of the connection after 554 | // unsubscribing from all channels, we do not decrement pending to a 555 | // negative value. 556 | // 557 | // The pending field is decremented after the reply is read to handle the 558 | // case where Receive is called before Send. 559 | c.mu.Lock() 560 | if c.pending > 0 { 561 | c.pending -= 1 562 | } 563 | c.mu.Unlock() 564 | if err, ok := reply.(Error); ok { 565 | return nil, err 566 | } 567 | return 568 | } 569 | 570 | func (c *conn) Do(cmd string, args ...interface{}) (interface{}, error) { 571 | c.mu.Lock() 572 | pending := c.pending 573 | c.pending = 0 574 | c.mu.Unlock() 575 | 576 | if cmd == "" && pending == 0 { 577 | return nil, nil 578 | } 579 | 580 | if c.writeTimeout != 0 { 581 | c.conn.SetWriteDeadline(time.Now().Add(c.writeTimeout)) 582 | } 583 | 584 | if cmd != "" { 585 | if err := c.writeCommand(cmd, args); err != nil { 586 | return nil, c.fatal(err) 587 | } 588 | } 589 | 590 | if err := c.bw.Flush(); err != nil { 591 | return nil, c.fatal(err) 592 | } 593 | 594 | if c.readTimeout != 0 { 595 | c.conn.SetReadDeadline(time.Now().Add(c.readTimeout)) 596 | } 597 | 598 | if cmd == "" { 599 | reply := make([]interface{}, pending) 600 | for i := range reply { 601 | r, e := c.readReply() 602 | if e != nil { 603 | return nil, c.fatal(e) 604 | } 605 | reply[i] = r 606 | } 607 | return reply, nil 608 | } 609 | 610 | var err error 611 | var reply interface{} 612 | for i := 0; i <= pending; i++ { 613 | var e error 614 | if reply, e = c.readReply(); e != nil { 615 | return nil, c.fatal(e) 616 | } 617 | if e, ok := reply.(Error); ok && err == nil { 618 | err = e 619 | } 620 | } 621 | return reply, err 622 | } 623 | -------------------------------------------------------------------------------- /redis/scan.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Gary Burd 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package redis 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "reflect" 21 | "strconv" 22 | "strings" 23 | "sync" 24 | ) 25 | 26 | func ensureLen(d reflect.Value, n int) { 27 | if n > d.Cap() { 28 | d.Set(reflect.MakeSlice(d.Type(), n, n)) 29 | } else { 30 | d.SetLen(n) 31 | } 32 | } 33 | 34 | func cannotConvert(d reflect.Value, s interface{}) error { 35 | var sname string 36 | switch s.(type) { 37 | case string: 38 | sname = "Redis simple string" 39 | case Error: 40 | sname = "Redis error" 41 | case int64: 42 | sname = "Redis integer" 43 | case []byte: 44 | sname = "Redis bulk string" 45 | case []interface{}: 46 | sname = "Redis array" 47 | default: 48 | sname = reflect.TypeOf(s).String() 49 | } 50 | return fmt.Errorf("cannot convert from %s to %s", sname, d.Type()) 51 | } 52 | 53 | func convertAssignBulkString(d reflect.Value, s []byte) (err error) { 54 | switch d.Type().Kind() { 55 | case reflect.Float32, reflect.Float64: 56 | var x float64 57 | x, err = strconv.ParseFloat(string(s), d.Type().Bits()) 58 | d.SetFloat(x) 59 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 60 | var x int64 61 | x, err = strconv.ParseInt(string(s), 10, d.Type().Bits()) 62 | d.SetInt(x) 63 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 64 | var x uint64 65 | x, err = strconv.ParseUint(string(s), 10, d.Type().Bits()) 66 | d.SetUint(x) 67 | case reflect.Bool: 68 | var x bool 69 | x, err = strconv.ParseBool(string(s)) 70 | d.SetBool(x) 71 | case reflect.String: 72 | d.SetString(string(s)) 73 | case reflect.Slice: 74 | if d.Type().Elem().Kind() != reflect.Uint8 { 75 | err = cannotConvert(d, s) 76 | } else { 77 | d.SetBytes(s) 78 | } 79 | default: 80 | err = cannotConvert(d, s) 81 | } 82 | return 83 | } 84 | 85 | func convertAssignInt(d reflect.Value, s int64) (err error) { 86 | switch d.Type().Kind() { 87 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 88 | d.SetInt(s) 89 | if d.Int() != s { 90 | err = strconv.ErrRange 91 | d.SetInt(0) 92 | } 93 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 94 | if s < 0 { 95 | err = strconv.ErrRange 96 | } else { 97 | x := uint64(s) 98 | d.SetUint(x) 99 | if d.Uint() != x { 100 | err = strconv.ErrRange 101 | d.SetUint(0) 102 | } 103 | } 104 | case reflect.Bool: 105 | d.SetBool(s != 0) 106 | default: 107 | err = cannotConvert(d, s) 108 | } 109 | return 110 | } 111 | 112 | func convertAssignValue(d reflect.Value, s interface{}) (err error) { 113 | if d.Kind() != reflect.Ptr { 114 | if d.CanAddr() { 115 | d2 := d.Addr() 116 | if d2.CanInterface() { 117 | if scanner, ok := d2.Interface().(Scanner); ok { 118 | return scanner.RedisScan(s) 119 | } 120 | } 121 | } 122 | } else if d.CanInterface() { 123 | // Already a reflect.Ptr 124 | if d.IsNil() { 125 | d.Set(reflect.New(d.Type().Elem())) 126 | } 127 | if scanner, ok := d.Interface().(Scanner); ok { 128 | return scanner.RedisScan(s) 129 | } 130 | } 131 | 132 | switch s := s.(type) { 133 | case []byte: 134 | err = convertAssignBulkString(d, s) 135 | case int64: 136 | err = convertAssignInt(d, s) 137 | default: 138 | err = cannotConvert(d, s) 139 | } 140 | return err 141 | } 142 | 143 | func convertAssignArray(d reflect.Value, s []interface{}) error { 144 | if d.Type().Kind() != reflect.Slice { 145 | return cannotConvert(d, s) 146 | } 147 | ensureLen(d, len(s)) 148 | for i := 0; i < len(s); i++ { 149 | if err := convertAssignValue(d.Index(i), s[i]); err != nil { 150 | return err 151 | } 152 | } 153 | return nil 154 | } 155 | 156 | func convertAssign(d interface{}, s interface{}) (err error) { 157 | if scanner, ok := d.(Scanner); ok { 158 | return scanner.RedisScan(s) 159 | } 160 | 161 | // Handle the most common destination types using type switches and 162 | // fall back to reflection for all other types. 163 | switch s := s.(type) { 164 | case nil: 165 | // ignore 166 | case []byte: 167 | switch d := d.(type) { 168 | case *string: 169 | *d = string(s) 170 | case *int: 171 | *d, err = strconv.Atoi(string(s)) 172 | case *bool: 173 | *d, err = strconv.ParseBool(string(s)) 174 | case *[]byte: 175 | *d = s 176 | case *interface{}: 177 | *d = s 178 | case nil: 179 | // skip value 180 | default: 181 | if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr { 182 | err = cannotConvert(d, s) 183 | } else { 184 | err = convertAssignBulkString(d.Elem(), s) 185 | } 186 | } 187 | case int64: 188 | switch d := d.(type) { 189 | case *int: 190 | x := int(s) 191 | if int64(x) != s { 192 | err = strconv.ErrRange 193 | x = 0 194 | } 195 | *d = x 196 | case *bool: 197 | *d = s != 0 198 | case *interface{}: 199 | *d = s 200 | case nil: 201 | // skip value 202 | default: 203 | if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr { 204 | err = cannotConvert(d, s) 205 | } else { 206 | err = convertAssignInt(d.Elem(), s) 207 | } 208 | } 209 | case string: 210 | switch d := d.(type) { 211 | case *string: 212 | *d = s 213 | case *interface{}: 214 | *d = s 215 | case nil: 216 | // skip value 217 | default: 218 | err = cannotConvert(reflect.ValueOf(d), s) 219 | } 220 | case []interface{}: 221 | switch d := d.(type) { 222 | case *[]interface{}: 223 | *d = s 224 | case *interface{}: 225 | *d = s 226 | case nil: 227 | // skip value 228 | default: 229 | if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr { 230 | err = cannotConvert(d, s) 231 | } else { 232 | err = convertAssignArray(d.Elem(), s) 233 | } 234 | } 235 | case Error: 236 | err = s 237 | default: 238 | err = cannotConvert(reflect.ValueOf(d), s) 239 | } 240 | return 241 | } 242 | 243 | // Scan copies from src to the values pointed at by dest. 244 | // 245 | // Scan uses RedisScan if available otherwise: 246 | // 247 | // The values pointed at by dest must be an integer, float, boolean, string, 248 | // []byte, interface{} or slices of these types. Scan uses the standard strconv 249 | // package to convert bulk strings to numeric and boolean types. 250 | // 251 | // If a dest value is nil, then the corresponding src value is skipped. 252 | // 253 | // If a src element is nil, then the corresponding dest value is not modified. 254 | // 255 | // To enable easy use of Scan in a loop, Scan returns the slice of src 256 | // following the copied values. 257 | func Scan(src []interface{}, dest ...interface{}) ([]interface{}, error) { 258 | if len(src) < len(dest) { 259 | return nil, errors.New("RedisGo-Async.Scan: array short") 260 | } 261 | var err error 262 | for i, d := range dest { 263 | err = convertAssign(d, src[i]) 264 | if err != nil { 265 | err = fmt.Errorf("RedisGo-Async.Scan: cannot assign to dest %d: %v", i, err) 266 | break 267 | } 268 | } 269 | return src[len(dest):], err 270 | } 271 | 272 | type fieldSpec struct { 273 | name string 274 | index []int 275 | omitEmpty bool 276 | } 277 | 278 | type structSpec struct { 279 | m map[string]*fieldSpec 280 | l []*fieldSpec 281 | } 282 | 283 | func (ss *structSpec) fieldSpec(name []byte) *fieldSpec { 284 | return ss.m[string(name)] 285 | } 286 | 287 | func compileStructSpec(t reflect.Type, depth map[string]int, index []int, ss *structSpec) { 288 | for i := 0; i < t.NumField(); i++ { 289 | f := t.Field(i) 290 | switch { 291 | case f.PkgPath != "" && !f.Anonymous: 292 | // Ignore unexported fields. 293 | case f.Anonymous: 294 | // TODO: Handle pointers. Requires change to decoder and 295 | // protection against infinite recursion. 296 | if f.Type.Kind() == reflect.Struct { 297 | compileStructSpec(f.Type, depth, append(index, i), ss) 298 | } 299 | default: 300 | fs := &fieldSpec{name: f.Name} 301 | tag := f.Tag.Get("redis") 302 | p := strings.Split(tag, ",") 303 | if len(p) > 0 { 304 | if p[0] == "-" { 305 | continue 306 | } 307 | if len(p[0]) > 0 { 308 | fs.name = p[0] 309 | } 310 | for _, s := range p[1:] { 311 | switch s { 312 | case "omitempty": 313 | fs.omitEmpty = true 314 | default: 315 | panic(fmt.Errorf("RedisGo-Async: unknown field tag %s for type %s", s, t.Name())) 316 | } 317 | } 318 | } 319 | d, found := depth[fs.name] 320 | if !found { 321 | d = 1 << 30 322 | } 323 | switch { 324 | case len(index) == d: 325 | // At same depth, remove from result. 326 | delete(ss.m, fs.name) 327 | j := 0 328 | for i := 0; i < len(ss.l); i++ { 329 | if fs.name != ss.l[i].name { 330 | ss.l[j] = ss.l[i] 331 | j += 1 332 | } 333 | } 334 | ss.l = ss.l[:j] 335 | case len(index) < d: 336 | fs.index = make([]int, len(index)+1) 337 | copy(fs.index, index) 338 | fs.index[len(index)] = i 339 | depth[fs.name] = len(index) 340 | ss.m[fs.name] = fs 341 | ss.l = append(ss.l, fs) 342 | } 343 | } 344 | } 345 | } 346 | 347 | var ( 348 | structSpecMutex sync.RWMutex 349 | structSpecCache = make(map[reflect.Type]*structSpec) 350 | defaultFieldSpec = &fieldSpec{} 351 | ) 352 | 353 | func structSpecForType(t reflect.Type) *structSpec { 354 | 355 | structSpecMutex.RLock() 356 | ss, found := structSpecCache[t] 357 | structSpecMutex.RUnlock() 358 | if found { 359 | return ss 360 | } 361 | 362 | structSpecMutex.Lock() 363 | defer structSpecMutex.Unlock() 364 | ss, found = structSpecCache[t] 365 | if found { 366 | return ss 367 | } 368 | 369 | ss = &structSpec{m: make(map[string]*fieldSpec)} 370 | compileStructSpec(t, make(map[string]int), nil, ss) 371 | structSpecCache[t] = ss 372 | return ss 373 | } 374 | 375 | var errScanStructValue = errors.New("RedisGo-Async.ScanStruct: value must be non-nil pointer to a struct") 376 | 377 | // ScanStruct scans alternating names and values from src to a struct. The 378 | // HGETALL and CONFIG GET commands return replies in this format. 379 | // 380 | // ScanStruct uses exported field names to match values in the response. Use 381 | // 'redis' field tag to override the name: 382 | // 383 | // Field int `redis:"myName"` 384 | // 385 | // Fields with the tag redis:"-" are ignored. 386 | // 387 | // Each field uses RedisScan if available otherwise: 388 | // Integer, float, boolean, string and []byte fields are supported. Scan uses the 389 | // standard strconv package to convert bulk string values to numeric and 390 | // boolean types. 391 | // 392 | // If a src element is nil, then the corresponding field is not modified. 393 | func ScanStruct(src []interface{}, dest interface{}) error { 394 | d := reflect.ValueOf(dest) 395 | if d.Kind() != reflect.Ptr || d.IsNil() { 396 | return errScanStructValue 397 | } 398 | d = d.Elem() 399 | if d.Kind() != reflect.Struct { 400 | return errScanStructValue 401 | } 402 | ss := structSpecForType(d.Type()) 403 | 404 | if len(src)%2 != 0 { 405 | return errors.New("RedisGo-Async.ScanStruct: number of values not a multiple of 2") 406 | } 407 | 408 | for i := 0; i < len(src); i += 2 { 409 | s := src[i+1] 410 | if s == nil { 411 | continue 412 | } 413 | name, ok := src[i].([]byte) 414 | if !ok { 415 | return fmt.Errorf("RedisGo-Async.ScanStruct: key %d not a bulk string value", i) 416 | } 417 | fs := ss.fieldSpec(name) 418 | if fs == nil { 419 | continue 420 | } 421 | if err := convertAssignValue(d.FieldByIndex(fs.index), s); err != nil { 422 | return fmt.Errorf("RedisGo-Async.ScanStruct: cannot assign field %s: %v", fs.name, err) 423 | } 424 | } 425 | return nil 426 | } 427 | 428 | var ( 429 | errScanSliceValue = errors.New("RedisGo-Async.ScanSlice: dest must be non-nil pointer to a struct") 430 | ) 431 | 432 | // ScanSlice scans src to the slice pointed to by dest. The elements the dest 433 | // slice must be integer, float, boolean, string, struct or pointer to struct 434 | // values. 435 | // 436 | // Struct fields must be integer, float, boolean or string values. All struct 437 | // fields are used unless a subset is specified using fieldNames. 438 | func ScanSlice(src []interface{}, dest interface{}, fieldNames ...string) error { 439 | d := reflect.ValueOf(dest) 440 | if d.Kind() != reflect.Ptr || d.IsNil() { 441 | return errScanSliceValue 442 | } 443 | d = d.Elem() 444 | if d.Kind() != reflect.Slice { 445 | return errScanSliceValue 446 | } 447 | 448 | isPtr := false 449 | t := d.Type().Elem() 450 | if t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct { 451 | isPtr = true 452 | t = t.Elem() 453 | } 454 | 455 | if t.Kind() != reflect.Struct { 456 | ensureLen(d, len(src)) 457 | for i, s := range src { 458 | if s == nil { 459 | continue 460 | } 461 | if err := convertAssignValue(d.Index(i), s); err != nil { 462 | return fmt.Errorf("RedisGo-Async.ScanSlice: cannot assign element %d: %v", i, err) 463 | } 464 | } 465 | return nil 466 | } 467 | 468 | ss := structSpecForType(t) 469 | fss := ss.l 470 | if len(fieldNames) > 0 { 471 | fss = make([]*fieldSpec, len(fieldNames)) 472 | for i, name := range fieldNames { 473 | fss[i] = ss.m[name] 474 | if fss[i] == nil { 475 | return fmt.Errorf("RedisGo-Async.ScanSlice: ScanSlice bad field name %s", name) 476 | } 477 | } 478 | } 479 | 480 | if len(fss) == 0 { 481 | return errors.New("RedisGo-Async.ScanSlice: no struct fields") 482 | } 483 | 484 | n := len(src) / len(fss) 485 | if n*len(fss) != len(src) { 486 | return errors.New("RedisGo-Async.ScanSlice: length not a multiple of struct field count") 487 | } 488 | 489 | ensureLen(d, n) 490 | for i := 0; i < n; i++ { 491 | d := d.Index(i) 492 | if isPtr { 493 | if d.IsNil() { 494 | d.Set(reflect.New(t)) 495 | } 496 | d = d.Elem() 497 | } 498 | for j, fs := range fss { 499 | s := src[i*len(fss)+j] 500 | if s == nil { 501 | continue 502 | } 503 | if err := convertAssignValue(d.FieldByIndex(fs.index), s); err != nil { 504 | return fmt.Errorf("RedisGo-Async.ScanSlice: cannot assign element %d to field %s: %v", i*len(fss)+j, fs.name, err) 505 | } 506 | } 507 | } 508 | return nil 509 | } 510 | 511 | // Args is a helper for constructing command arguments from structured values. 512 | type Args []interface{} 513 | 514 | // Add returns the result of appending value to args. 515 | func (args Args) Add(value ...interface{}) Args { 516 | return append(args, value...) 517 | } 518 | 519 | // AddFlat returns the result of appending the flattened value of v to args. 520 | // 521 | // Maps are flattened by appending the alternating keys and map values to args. 522 | // 523 | // Slices are flattened by appending the slice elements to args. 524 | // 525 | // Structs are flattened by appending the alternating names and values of 526 | // exported fields to args. If v is a nil struct pointer, then nothing is 527 | // appended. The 'redis' field tag overrides struct field names. See ScanStruct 528 | // for more information on the use of the 'redis' field tag. 529 | // 530 | // Other types are appended to args as is. 531 | func (args Args) AddFlat(v interface{}) Args { 532 | rv := reflect.ValueOf(v) 533 | switch rv.Kind() { 534 | case reflect.Struct: 535 | args = flattenStruct(args, rv) 536 | case reflect.Slice: 537 | for i := 0; i < rv.Len(); i++ { 538 | args = append(args, rv.Index(i).Interface()) 539 | } 540 | case reflect.Map: 541 | for _, k := range rv.MapKeys() { 542 | args = append(args, k.Interface(), rv.MapIndex(k).Interface()) 543 | } 544 | case reflect.Ptr: 545 | if rv.Type().Elem().Kind() == reflect.Struct { 546 | if !rv.IsNil() { 547 | args = flattenStruct(args, rv.Elem()) 548 | } 549 | } else { 550 | args = append(args, v) 551 | } 552 | default: 553 | args = append(args, v) 554 | } 555 | return args 556 | } 557 | 558 | func flattenStruct(args Args, v reflect.Value) Args { 559 | ss := structSpecForType(v.Type()) 560 | for _, fs := range ss.l { 561 | fv := v.FieldByIndex(fs.index) 562 | if fs.omitEmpty { 563 | var empty = false 564 | switch fv.Kind() { 565 | case reflect.Array, reflect.Map, reflect.Slice, reflect.String: 566 | empty = fv.Len() == 0 567 | case reflect.Bool: 568 | empty = !fv.Bool() 569 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 570 | empty = fv.Int() == 0 571 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 572 | empty = fv.Uint() == 0 573 | case reflect.Float32, reflect.Float64: 574 | empty = fv.Float() == 0 575 | case reflect.Interface, reflect.Ptr: 576 | empty = fv.IsNil() 577 | } 578 | if empty { 579 | continue 580 | } 581 | } 582 | args = append(args, fs.name, fv.Interface()) 583 | } 584 | return args 585 | } 586 | -------------------------------------------------------------------------------- /redis/conn_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Gary Burd 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 | // not use this file except in compliance with the License. You may obtain 5 | // a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 | // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 | // License for the specific language governing permissions and limitations 13 | // under the License. 14 | 15 | package redis_test 16 | 17 | import ( 18 | "bytes" 19 | "io" 20 | "math" 21 | "net" 22 | "os" 23 | "reflect" 24 | "strings" 25 | "testing" 26 | "time" 27 | 28 | "github.com/gistao/RedisGo-Async/redis" 29 | ) 30 | 31 | type testConn struct { 32 | io.Reader 33 | io.Writer 34 | } 35 | 36 | func (*testConn) Close() error { return nil } 37 | func (*testConn) LocalAddr() net.Addr { return nil } 38 | func (*testConn) RemoteAddr() net.Addr { return nil } 39 | func (*testConn) SetDeadline(t time.Time) error { return nil } 40 | func (*testConn) SetReadDeadline(t time.Time) error { return nil } 41 | func (*testConn) SetWriteDeadline(t time.Time) error { return nil } 42 | 43 | func dialTestConn(r io.Reader, w io.Writer) redis.DialOption { 44 | return redis.DialNetDial(func(net, addr string) (net.Conn, error) { 45 | return &testConn{Reader: r, Writer: w}, nil 46 | }) 47 | } 48 | 49 | type durationArg struct { 50 | time.Duration 51 | } 52 | 53 | func (t durationArg) RedisArg() interface{} { 54 | return t.Seconds() 55 | } 56 | 57 | var writeTests = []struct { 58 | args []interface{} 59 | expected string 60 | }{ 61 | { 62 | []interface{}{"SET", "key", "value"}, 63 | "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n", 64 | }, 65 | { 66 | []interface{}{"SET", "key", "value"}, 67 | "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n", 68 | }, 69 | { 70 | []interface{}{"SET", "key", byte(100)}, 71 | "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$3\r\n100\r\n", 72 | }, 73 | { 74 | []interface{}{"SET", "key", 100}, 75 | "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$3\r\n100\r\n", 76 | }, 77 | { 78 | []interface{}{"SET", "key", int64(math.MinInt64)}, 79 | "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$20\r\n-9223372036854775808\r\n", 80 | }, 81 | { 82 | []interface{}{"SET", "key", float64(1349673917.939762)}, 83 | "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$21\r\n1.349673917939762e+09\r\n", 84 | }, 85 | { 86 | []interface{}{"SET", "key", ""}, 87 | "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$0\r\n\r\n", 88 | }, 89 | { 90 | []interface{}{"SET", "key", nil}, 91 | "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$0\r\n\r\n", 92 | }, 93 | { 94 | []interface{}{"SET", "key", durationArg{time.Minute}}, 95 | "*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$2\r\n60\r\n", 96 | }, 97 | { 98 | []interface{}{"ECHO", true, false}, 99 | "*3\r\n$4\r\nECHO\r\n$1\r\n1\r\n$1\r\n0\r\n", 100 | }, 101 | } 102 | 103 | func TestWrite(t *testing.T) { 104 | for _, tt := range writeTests { 105 | var buf bytes.Buffer 106 | c, _ := redis.Dial("", "", dialTestConn(nil, &buf)) 107 | err := c.Send(tt.args[0].(string), tt.args[1:]...) 108 | if err != nil { 109 | t.Errorf("Send(%v) returned error %v", tt.args, err) 110 | continue 111 | } 112 | c.Flush() 113 | actual := buf.String() 114 | if actual != tt.expected { 115 | t.Errorf("Send(%v) = %q, want %q", tt.args, actual, tt.expected) 116 | } 117 | } 118 | } 119 | 120 | var errorSentinel = &struct{}{} 121 | 122 | var readTests = []struct { 123 | reply string 124 | expected interface{} 125 | }{ 126 | { 127 | "+OK\r\n", 128 | "OK", 129 | }, 130 | { 131 | "+PONG\r\n", 132 | "PONG", 133 | }, 134 | { 135 | "@OK\r\n", 136 | errorSentinel, 137 | }, 138 | { 139 | "$6\r\nfoobar\r\n", 140 | []byte("foobar"), 141 | }, 142 | { 143 | "$-1\r\n", 144 | nil, 145 | }, 146 | { 147 | ":1\r\n", 148 | int64(1), 149 | }, 150 | { 151 | ":-2\r\n", 152 | int64(-2), 153 | }, 154 | { 155 | "*0\r\n", 156 | []interface{}{}, 157 | }, 158 | { 159 | "*-1\r\n", 160 | nil, 161 | }, 162 | { 163 | "*4\r\n$3\r\nfoo\r\n$3\r\nbar\r\n$5\r\nHello\r\n$5\r\nWorld\r\n", 164 | []interface{}{[]byte("foo"), []byte("bar"), []byte("Hello"), []byte("World")}, 165 | }, 166 | { 167 | "*3\r\n$3\r\nfoo\r\n$-1\r\n$3\r\nbar\r\n", 168 | []interface{}{[]byte("foo"), nil, []byte("bar")}, 169 | }, 170 | 171 | { 172 | // "x" is not a valid length 173 | "$x\r\nfoobar\r\n", 174 | errorSentinel, 175 | }, 176 | { 177 | // -2 is not a valid length 178 | "$-2\r\n", 179 | errorSentinel, 180 | }, 181 | { 182 | // "x" is not a valid integer 183 | ":x\r\n", 184 | errorSentinel, 185 | }, 186 | { 187 | // missing \r\n following value 188 | "$6\r\nfoobar", 189 | errorSentinel, 190 | }, 191 | { 192 | // short value 193 | "$6\r\nxx", 194 | errorSentinel, 195 | }, 196 | { 197 | // long value 198 | "$6\r\nfoobarx\r\n", 199 | errorSentinel, 200 | }, 201 | } 202 | 203 | func TestRead(t *testing.T) { 204 | for _, tt := range readTests { 205 | c, _ := redis.Dial("", "", dialTestConn(strings.NewReader(tt.reply), nil)) 206 | actual, err := c.Receive() 207 | if tt.expected == errorSentinel { 208 | if err == nil { 209 | t.Errorf("Receive(%q) did not return expected error", tt.reply) 210 | } 211 | } else { 212 | if err != nil { 213 | t.Errorf("Receive(%q) returned error %v", tt.reply, err) 214 | continue 215 | } 216 | if !reflect.DeepEqual(actual, tt.expected) { 217 | t.Errorf("Receive(%q) = %v, want %v", tt.reply, actual, tt.expected) 218 | } 219 | } 220 | } 221 | } 222 | 223 | var testCommands = []struct { 224 | args []interface{} 225 | expected interface{} 226 | }{ 227 | { 228 | []interface{}{"PING"}, 229 | "PONG", 230 | }, 231 | { 232 | []interface{}{"SET", "foo", "bar"}, 233 | "OK", 234 | }, 235 | { 236 | []interface{}{"GET", "foo"}, 237 | []byte("bar"), 238 | }, 239 | { 240 | []interface{}{"GET", "nokey"}, 241 | nil, 242 | }, 243 | { 244 | []interface{}{"MGET", "nokey", "foo"}, 245 | []interface{}{nil, []byte("bar")}, 246 | }, 247 | { 248 | []interface{}{"INCR", "mycounter"}, 249 | int64(1), 250 | }, 251 | { 252 | []interface{}{"LPUSH", "mylist", "foo"}, 253 | int64(1), 254 | }, 255 | { 256 | []interface{}{"LPUSH", "mylist", "bar"}, 257 | int64(2), 258 | }, 259 | { 260 | []interface{}{"LRANGE", "mylist", 0, -1}, 261 | []interface{}{[]byte("bar"), []byte("foo")}, 262 | }, 263 | { 264 | []interface{}{"MULTI"}, 265 | "OK", 266 | }, 267 | { 268 | []interface{}{"LRANGE", "mylist", 0, -1}, 269 | "QUEUED", 270 | }, 271 | { 272 | []interface{}{"PING"}, 273 | "QUEUED", 274 | }, 275 | { 276 | []interface{}{"EXEC"}, 277 | []interface{}{ 278 | []interface{}{[]byte("bar"), []byte("foo")}, 279 | "PONG", 280 | }, 281 | }, 282 | } 283 | 284 | func TestDoCommands(t *testing.T) { 285 | c, err := redis.DialDefaultServer() 286 | if err != nil { 287 | t.Fatalf("error connection to database, %v", err) 288 | } 289 | defer c.Close() 290 | 291 | for _, cmd := range testCommands { 292 | actual, err := c.Do(cmd.args[0].(string), cmd.args[1:]...) 293 | if err != nil { 294 | t.Errorf("Do(%v) returned error %v", cmd.args, err) 295 | continue 296 | } 297 | if !reflect.DeepEqual(actual, cmd.expected) { 298 | t.Errorf("Do(%v) = %v, want %v", cmd.args, actual, cmd.expected) 299 | } 300 | } 301 | } 302 | 303 | func TestPipelineCommands(t *testing.T) { 304 | c, err := redis.DialDefaultServer() 305 | if err != nil { 306 | t.Fatalf("error connection to database, %v", err) 307 | } 308 | defer c.Close() 309 | 310 | for _, cmd := range testCommands { 311 | if err := c.Send(cmd.args[0].(string), cmd.args[1:]...); err != nil { 312 | t.Fatalf("Send(%v) returned error %v", cmd.args, err) 313 | } 314 | } 315 | if err := c.Flush(); err != nil { 316 | t.Errorf("Flush() returned error %v", err) 317 | } 318 | for _, cmd := range testCommands { 319 | actual, err := c.Receive() 320 | if err != nil { 321 | t.Fatalf("Receive(%v) returned error %v", cmd.args, err) 322 | } 323 | if !reflect.DeepEqual(actual, cmd.expected) { 324 | t.Errorf("Receive(%v) = %v, want %v", cmd.args, actual, cmd.expected) 325 | } 326 | } 327 | } 328 | 329 | func TestBlankCommmand(t *testing.T) { 330 | c, err := redis.DialDefaultServer() 331 | if err != nil { 332 | t.Fatalf("error connection to database, %v", err) 333 | } 334 | defer c.Close() 335 | 336 | for _, cmd := range testCommands { 337 | if err := c.Send(cmd.args[0].(string), cmd.args[1:]...); err != nil { 338 | t.Fatalf("Send(%v) returned error %v", cmd.args, err) 339 | } 340 | } 341 | reply, err := redis.Values(c.Do("")) 342 | if err != nil { 343 | t.Fatalf("Do() returned error %v", err) 344 | } 345 | if len(reply) != len(testCommands) { 346 | t.Fatalf("len(reply)=%d, want %d", len(reply), len(testCommands)) 347 | } 348 | for i, cmd := range testCommands { 349 | actual := reply[i] 350 | if !reflect.DeepEqual(actual, cmd.expected) { 351 | t.Errorf("Receive(%v) = %v, want %v", cmd.args, actual, cmd.expected) 352 | } 353 | } 354 | } 355 | 356 | func TestRecvBeforeSend(t *testing.T) { 357 | c, err := redis.DialDefaultServer() 358 | if err != nil { 359 | t.Fatalf("error connection to database, %v", err) 360 | } 361 | defer c.Close() 362 | done := make(chan struct{}) 363 | go func() { 364 | c.Receive() 365 | close(done) 366 | }() 367 | time.Sleep(time.Millisecond) 368 | c.Send("PING") 369 | c.Flush() 370 | <-done 371 | _, err = c.Do("") 372 | if err != nil { 373 | t.Fatalf("error=%v", err) 374 | } 375 | } 376 | 377 | func TestError(t *testing.T) { 378 | c, err := redis.DialDefaultServer() 379 | if err != nil { 380 | t.Fatalf("error connection to database, %v", err) 381 | } 382 | defer c.Close() 383 | 384 | c.Do("SET", "key", "val") 385 | _, err = c.Do("HSET", "key", "fld", "val") 386 | if err == nil { 387 | t.Errorf("Expected err for HSET on string key.") 388 | } 389 | if c.Err() != nil { 390 | t.Errorf("Conn has Err()=%v, expect nil", c.Err()) 391 | } 392 | _, err = c.Do("SET", "key", "val") 393 | if err != nil { 394 | t.Errorf("Do(SET, key, val) returned error %v, expected nil.", err) 395 | } 396 | } 397 | 398 | func TestReadTimeout(t *testing.T) { 399 | l, err := net.Listen("tcp", "127.0.0.1:0") 400 | if err != nil { 401 | t.Fatalf("net.Listen returned %v", err) 402 | } 403 | defer l.Close() 404 | 405 | go func() { 406 | for { 407 | c, err := l.Accept() 408 | if err != nil { 409 | return 410 | } 411 | go func() { 412 | time.Sleep(time.Second) 413 | c.Write([]byte("+OK\r\n")) 414 | c.Close() 415 | }() 416 | } 417 | }() 418 | 419 | // Do 420 | 421 | c1, err := redis.Dial(l.Addr().Network(), l.Addr().String(), redis.DialReadTimeout(time.Millisecond)) 422 | if err != nil { 423 | t.Fatalf("redis.Dial returned %v", err) 424 | } 425 | defer c1.Close() 426 | 427 | _, err = c1.Do("PING") 428 | if err == nil { 429 | t.Fatalf("c1.Do() returned nil, expect error") 430 | } 431 | if c1.Err() == nil { 432 | t.Fatalf("c1.Err() = nil, expect error") 433 | } 434 | 435 | // Send/Flush/Receive 436 | 437 | c2, err := redis.Dial(l.Addr().Network(), l.Addr().String(), redis.DialReadTimeout(time.Millisecond)) 438 | if err != nil { 439 | t.Fatalf("redis.Dial returned %v", err) 440 | } 441 | defer c2.Close() 442 | 443 | c2.Send("PING") 444 | c2.Flush() 445 | _, err = c2.Receive() 446 | if err == nil { 447 | t.Fatalf("c2.Receive() returned nil, expect error") 448 | } 449 | if c2.Err() == nil { 450 | t.Fatalf("c2.Err() = nil, expect error") 451 | } 452 | } 453 | 454 | var dialErrors = []struct { 455 | rawurl string 456 | expectedError string 457 | }{ 458 | { 459 | "localhost", 460 | "invalid redis URL scheme", 461 | }, 462 | // The error message for invalid hosts is different in different 463 | // versions of Go, so just check that there is an error message. 464 | { 465 | "redis://weird url", 466 | "", 467 | }, 468 | { 469 | "redis://foo:bar:baz", 470 | "", 471 | }, 472 | { 473 | "http://www.google.com", 474 | "invalid redis URL scheme: http", 475 | }, 476 | { 477 | "redis://localhost:6379/abc123", 478 | "invalid database: abc123", 479 | }, 480 | } 481 | 482 | func TestDialURLErrors(t *testing.T) { 483 | for _, d := range dialErrors { 484 | _, err := redis.DialURL(d.rawurl) 485 | if err == nil || !strings.Contains(err.Error(), d.expectedError) { 486 | t.Errorf("DialURL did not return expected error (expected %v to contain %s)", err, d.expectedError) 487 | } 488 | } 489 | } 490 | 491 | func TestDialURLPort(t *testing.T) { 492 | checkPort := func(network, address string) (net.Conn, error) { 493 | if address != "localhost:6379" { 494 | t.Errorf("DialURL did not set port to 6379 by default (got %v)", address) 495 | } 496 | return nil, nil 497 | } 498 | _, err := redis.DialURL("redis://localhost", redis.DialNetDial(checkPort)) 499 | if err != nil { 500 | t.Error("dial error:", err) 501 | } 502 | } 503 | 504 | func TestDialURLHost(t *testing.T) { 505 | checkHost := func(network, address string) (net.Conn, error) { 506 | if address != "localhost:6379" { 507 | t.Errorf("DialURL did not set host to localhost by default (got %v)", address) 508 | } 509 | return nil, nil 510 | } 511 | _, err := redis.DialURL("redis://:6379", redis.DialNetDial(checkHost)) 512 | if err != nil { 513 | t.Error("dial error:", err) 514 | } 515 | } 516 | 517 | func TestDialURLPassword(t *testing.T) { 518 | var buf bytes.Buffer 519 | _, err := redis.DialURL("redis://x:abc123@localhost", dialTestConn(strings.NewReader("+OK\r\n"), &buf)) 520 | if err != nil { 521 | t.Error("dial error:", err) 522 | } 523 | expected := "*2\r\n$4\r\nAUTH\r\n$6\r\nabc123\r\n" 524 | actual := buf.String() 525 | if actual != expected { 526 | t.Errorf("commands = %q, want %q", actual, expected) 527 | } 528 | } 529 | 530 | func TestDialURLDatabase(t *testing.T) { 531 | var buf3 bytes.Buffer 532 | _, err3 := redis.DialURL("redis://localhost/3", dialTestConn(strings.NewReader("+OK\r\n"), &buf3)) 533 | if err3 != nil { 534 | t.Error("dial error:", err3) 535 | } 536 | expected3 := "*2\r\n$6\r\nSELECT\r\n$1\r\n3\r\n" 537 | actual3 := buf3.String() 538 | if actual3 != expected3 { 539 | t.Errorf("commands = %q, want %q", actual3, expected3) 540 | } 541 | // empty DB means 0 542 | var buf0 bytes.Buffer 543 | _, err0 := redis.DialURL("redis://localhost/", dialTestConn(strings.NewReader("+OK\r\n"), &buf0)) 544 | if err0 != nil { 545 | t.Error("dial error:", err0) 546 | } 547 | expected0 := "" 548 | actual0 := buf0.String() 549 | if actual0 != expected0 { 550 | t.Errorf("commands = %q, want %q", actual0, expected0) 551 | } 552 | } 553 | 554 | // Connect to local instance of Redis running on the default port. 555 | func ExampleDial() { 556 | c, err := redis.Dial("tcp", ":6379") 557 | if err != nil { 558 | // handle error 559 | } 560 | defer c.Close() 561 | } 562 | 563 | // Connect to remote instance of Redis using a URL. 564 | func ExampleDialURL() { 565 | c, err := redis.DialURL(os.Getenv("REDIS_URL")) 566 | if err != nil { 567 | // handle connection error 568 | } 569 | defer c.Close() 570 | } 571 | 572 | // TextExecError tests handling of errors in a transaction. See 573 | // http://redis.io/topics/transactions for information on how Redis handles 574 | // errors in a transaction. 575 | func TestExecError(t *testing.T) { 576 | c, err := redis.DialDefaultServer() 577 | if err != nil { 578 | t.Fatalf("error connection to database, %v", err) 579 | } 580 | defer c.Close() 581 | 582 | // Execute commands that fail before EXEC is called. 583 | 584 | c.Do("DEL", "k0") 585 | c.Do("ZADD", "k0", 0, 0) 586 | c.Send("MULTI") 587 | c.Send("NOTACOMMAND", "k0", 0, 0) 588 | c.Send("ZINCRBY", "k0", 0, 0) 589 | v, err := c.Do("EXEC") 590 | if err == nil { 591 | t.Fatalf("EXEC returned values %v, expected error", v) 592 | } 593 | 594 | // Execute commands that fail after EXEC is called. The first command 595 | // returns an error. 596 | 597 | c.Do("DEL", "k1") 598 | c.Do("ZADD", "k1", 0, 0) 599 | c.Send("MULTI") 600 | c.Send("HSET", "k1", 0, 0) 601 | c.Send("ZINCRBY", "k1", 0, 0) 602 | v, err = c.Do("EXEC") 603 | if err != nil { 604 | t.Fatalf("EXEC returned error %v", err) 605 | } 606 | 607 | vs, err := redis.Values(v, nil) 608 | if err != nil { 609 | t.Fatalf("Values(v) returned error %v", err) 610 | } 611 | 612 | if len(vs) != 2 { 613 | t.Fatalf("len(vs) == %d, want 2", len(vs)) 614 | } 615 | 616 | if _, ok := vs[0].(error); !ok { 617 | t.Fatalf("first result is type %T, expected error", vs[0]) 618 | } 619 | 620 | if _, ok := vs[1].([]byte); !ok { 621 | t.Fatalf("second result is type %T, expected []byte", vs[1]) 622 | } 623 | 624 | // Execute commands that fail after EXEC is called. The second command 625 | // returns an error. 626 | 627 | c.Do("ZADD", "k2", 0, 0) 628 | c.Send("MULTI") 629 | c.Send("ZINCRBY", "k2", 0, 0) 630 | c.Send("HSET", "k2", 0, 0) 631 | v, err = c.Do("EXEC") 632 | if err != nil { 633 | t.Fatalf("EXEC returned error %v", err) 634 | } 635 | 636 | vs, err = redis.Values(v, nil) 637 | if err != nil { 638 | t.Fatalf("Values(v) returned error %v", err) 639 | } 640 | 641 | if len(vs) != 2 { 642 | t.Fatalf("len(vs) == %d, want 2", len(vs)) 643 | } 644 | 645 | if _, ok := vs[0].([]byte); !ok { 646 | t.Fatalf("first result is type %T, expected []byte", vs[0]) 647 | } 648 | 649 | if _, ok := vs[1].(error); !ok { 650 | t.Fatalf("second result is type %T, expected error", vs[2]) 651 | } 652 | } 653 | 654 | func BenchmarkDoEmpty(b *testing.B) { 655 | b.StopTimer() 656 | c, err := redis.DialDefaultServer() 657 | if err != nil { 658 | b.Fatal(err) 659 | } 660 | defer c.Close() 661 | b.StartTimer() 662 | for i := 0; i < b.N; i++ { 663 | if _, err := c.Do(""); err != nil { 664 | b.Fatal(err) 665 | } 666 | } 667 | } 668 | 669 | func BenchmarkDoPing(b *testing.B) { 670 | b.StopTimer() 671 | c, err := redis.DialDefaultServer() 672 | if err != nil { 673 | b.Fatal(err) 674 | } 675 | defer c.Close() 676 | b.StartTimer() 677 | for i := 0; i < b.N; i++ { 678 | if _, err := c.Do("PING"); err != nil { 679 | b.Fatal(err) 680 | } 681 | } 682 | } 683 | --------------------------------------------------------------------------------