├── LICENSE ├── README.md ├── batch.go ├── cluster.go ├── crc16.go ├── doc.go ├── example ├── example1.go ├── example2.go ├── example3.go └── example4.go ├── multi.go ├── node.go ├── node_test.go ├── reply.go └── reply_test.go /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-redis-cluster 2 | go-redis-cluster is a golang implementation of redis client based on Gary Burd's 3 | [Redigo](https://github.com/garyburd/redigo). It caches slot info at local and 4 | updates it automatically when cluster change. The client manages a connection pool 5 | for each node, uses goroutine to execute as concurrently as possible, which leads 6 | to its high efficiency and low lantency. 7 | 8 | **Supported**: 9 | * Most commands of keys, strings, lists, sets, sorted sets, hashes. 10 | * MGET/MSET 11 | * Pipelining 12 | 13 | **NOT supported**: 14 | * Cluster commands 15 | * Pub/Sub 16 | * Transaction 17 | * Lua script 18 | 19 | ## Installation 20 | Install go-redis-cluster with go tool: 21 | ``` 22 | go get github.com/gitstliu/go-redis-cluster 23 | ``` 24 | 25 | ## Usage 26 | To use redis cluster, you need import the package and create a new cluster client 27 | with an options: 28 | ```go 29 | import "github.com/gitstliu/go-redis-cluster" 30 | 31 | cluster, err := redis.NewCluster( 32 | &redis.Options{ 33 | StartNodes: []string{"127.0.0.1:7000", "127.0.0.1:7001", "127.0.0.1:7002"}, 34 | ConnTimeout: 50 * time.Millisecond, 35 | ReadTimeout: 50 * time.Millisecond, 36 | WriteTimeout: 50 * time.Millisecond, 37 | KeepAlive: 16, 38 | AliveTime: 60 * time.Second, 39 | }) 40 | ``` 41 | 42 | ### Basic 43 | go-redis-cluster has compatible interface to [Redigo](https://github.com/garyburd/redigo), 44 | which uses a print-like API for all redis commands. When executing a command, it need a key 45 | to hash to a slot, then find the corresponding redis node. Do method will choose first 46 | argument in args as the key, so commands which are independent from keys are not supported, 47 | such as SYNC, BGSAVE, RANDOMKEY, etc. 48 | 49 | **RESTRICTION**: Please be sure the first argument in args is key. 50 | 51 | See full redis commands: http://www.redis.io/commands 52 | 53 | ```go 54 | cluster.Do("SET", "foo", "bar") 55 | cluster.Do("INCR", "mycount", 1) 56 | cluster.Do("LPUSH", "mylist", "foo", "bar") 57 | cluster.Do("HMSET", "myhash", "f1", "foo", "f2", "bar") 58 | ``` 59 | You can use help functions to convert reply to int, float, string, etc. 60 | ```go 61 | reply, err := Int(cluster.Do("INCR", "mycount", 1)) 62 | reply, err := String(cluster.Do("GET", "foo")) 63 | reply, err := Strings(cluster.Do("LRANGE", "mylist", 0, -1)) 64 | reply, err := StringMap(cluster.Do("HGETALL", "myhash")) 65 | ``` 66 | Also, you can use Values and Scan to convert replies to multiple values with different types. 67 | ```go 68 | _, err := cluster.Do("MSET", "key1", "foo", "key2", 1024, "key3", 3.14, "key4", "false") 69 | reply, err := Values(cluster.Do("MGET", "key1", "key2", "key3", "key4")) 70 | var val1 string 71 | var val2 int 72 | reply, err = Scan(reply, &val1, &val2) 73 | var val3 float64 74 | reply, err = Scan(reply, &val3) 75 | var val4 bool 76 | reply, err = Scan(reply, &val4) 77 | 78 | ``` 79 | 80 | ### Multi-keys 81 | Mutiple keys command - MGET/MSET are supported using result aggregation. 82 | Processing steps are as follows: 83 | - First, split the keys into multiple nodes according to their hash slot. 84 | - Then, start a goroutine for each node to excute MGET/MSET commands and wait them finish. 85 | - Last, collect and rerange all replies, return back to caller. 86 | 87 | **NOTE**: Since the keys may spread across mutiple node, there's no atomicity gurantee that 88 | all keys will be set at once. It's possible that some keys are set while others are not. 89 | 90 | ### Pipelining 91 | Pipelining is supported through the Batch interface. You can put multiple commands into a 92 | batch as long as it is supported by Do method. RunBatch will split these command to distinct 93 | nodes and start a goroutine for each node. Commands hash to same nodes will be merged and sent 94 | using pipelining. After all commands done, it rearrange results as MGET/MSET do. Result is a 95 | slice of each command's reply, you can use Scan to convert them to other types. 96 | ```go 97 | batch := cluster.NewBatch() 98 | err = batch.Put("LPUSH", "country_list", "France") 99 | err = batch.Put("LPUSH", "country_list", "Italy") 100 | err = batch.Put("LPUSH", "country_list", "Germany") 101 | err = batch.Put("INCRBY", "countries", 3) 102 | err = batch.Put("LRANGE", "country_list", 0, -1) 103 | reply, err = cluster.RunBatch(batch) 104 | 105 | var resp int 106 | for i := 0; i < 4; i++ { 107 | reply, err = redis.Scan(reply, &resp) 108 | } 109 | 110 | countries, err := Strings(reply[0], nil) 111 | ``` 112 | 113 | ## Contact 114 | Bug reports and feature requests are welcome. 115 | If you have any question, please email me gitstliu@163.com. 116 | 117 | ## License 118 | go-redis-cluster is available under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.html). 119 | -------------------------------------------------------------------------------- /batch.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Joel Wu 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 | "fmt" 19 | ) 20 | 21 | // Batch pack multiple commands, which should be supported by Do method. 22 | type Batch struct { 23 | cluster *Cluster 24 | batches []nodeBatch 25 | index []int 26 | } 27 | 28 | type nodeBatch struct { 29 | node *redisNode 30 | cmds []nodeCommand 31 | 32 | err error 33 | done chan int 34 | } 35 | 36 | type nodeCommand struct { 37 | cmd string 38 | args []interface{} 39 | reply interface{} 40 | err error 41 | } 42 | 43 | // NewBatch create a new batch to pack mutiple commands. 44 | func (cluster *Cluster) NewBatch() *Batch { 45 | return &Batch{ 46 | cluster: cluster, 47 | batches: make([]nodeBatch, 0), 48 | index: make([]int, 0), 49 | } 50 | } 51 | 52 | // Put add a redis command to batch, DO NOT put MGET/MSET/MSETNX. 53 | func (batch *Batch) Put(cmd string, args ...interface{}) error { 54 | if len(args) < 1 { 55 | return fmt.Errorf("Put: no key found in args") 56 | } 57 | 58 | if cmd == "MGET" || cmd == "MSET" || cmd == "MSETNX" { 59 | return fmt.Errorf("Put: %s not supported", cmd) 60 | } 61 | 62 | node, err := batch.cluster.getNodeByKey(args[0]) 63 | if err != nil { 64 | return fmt.Errorf("Put: %v", err) 65 | } 66 | 67 | var i int 68 | for i = 0; i < len(batch.batches); i++ { 69 | if batch.batches[i].node == node { 70 | batch.batches[i].cmds = append(batch.batches[i].cmds, 71 | nodeCommand{cmd: cmd, args: args}) 72 | 73 | batch.index = append(batch.index, i) 74 | break 75 | } 76 | } 77 | 78 | if i == len(batch.batches) { 79 | batch.batches = append(batch.batches, 80 | nodeBatch{ 81 | node: node, 82 | cmds: []nodeCommand{{cmd: cmd, args: args}}, 83 | done: make(chan int)}) 84 | batch.index = append(batch.index, i) 85 | } 86 | 87 | return nil 88 | } 89 | 90 | // RunBatch execute commands in batch simutaneously. If multiple commands are 91 | // directed to the same node, they will be merged and sent at once using pipeling. 92 | func (cluster *Cluster) RunBatch(bat *Batch) ([]interface{}, error) { 93 | for i := range bat.batches { 94 | go doBatch(&bat.batches[i]) 95 | } 96 | 97 | for i := range bat.batches { 98 | <-bat.batches[i].done 99 | } 100 | 101 | var replies []interface{} 102 | for _, i := range bat.index { 103 | if bat.batches[i].err != nil { 104 | return nil, bat.batches[i].err 105 | } 106 | 107 | replies = append(replies, bat.batches[i].cmds[0].reply) 108 | bat.batches[i].cmds = bat.batches[i].cmds[1:] 109 | } 110 | 111 | return replies, nil 112 | } 113 | 114 | func doBatch(batch *nodeBatch) { 115 | conn, err := batch.node.getConn() 116 | if err != nil { 117 | batch.err = err 118 | batch.done <- 1 119 | return 120 | } 121 | 122 | for i := range batch.cmds { 123 | conn.send(batch.cmds[i].cmd, batch.cmds[i].args...) 124 | } 125 | 126 | err = conn.flush() 127 | if err != nil { 128 | batch.err = err 129 | conn.shutdown() 130 | batch.done <- 1 131 | return 132 | } 133 | 134 | for i := range batch.cmds { 135 | reply, err := conn.receive() 136 | if err != nil { 137 | batch.err = err 138 | conn.shutdown() 139 | batch.done <- 1 140 | return 141 | } 142 | 143 | batch.cmds[i].reply, batch.cmds[i].err = reply, err 144 | } 145 | 146 | batch.node.releaseConn(conn) 147 | batch.done <- 1 148 | } 149 | -------------------------------------------------------------------------------- /cluster.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "fmt" 7 | "time" 8 | "sync" 9 | "strings" 10 | "strconv" 11 | ) 12 | 13 | // Options is used to initialize a new redis cluster. 14 | type Options struct { 15 | StartNodes []string // Startup nodes 16 | 17 | ConnTimeout time.Duration // Connection timeout 18 | ReadTimeout time.Duration // Read timeout 19 | WriteTimeout time.Duration // Write timeout 20 | 21 | KeepAlive int // Maximum keep alive connecion in each node 22 | AliveTime time.Duration // Keep alive timeout 23 | } 24 | 25 | // Cluster is a redis client that manage connections to redis nodes, 26 | // cache and update cluster info, and execute all kinds of commands. 27 | // Multiple goroutines may invoke methods on a cluster simutaneously. 28 | type Cluster struct { 29 | slots [kClusterSlots]*redisNode 30 | nodes map[string]*redisNode 31 | 32 | connTimeout time.Duration 33 | readTimeout time.Duration 34 | writeTimeout time.Duration 35 | 36 | keepAlive int 37 | aliveTime time.Duration 38 | 39 | updateTime time.Time 40 | updateList chan updateMesg 41 | 42 | rwLock sync.RWMutex 43 | 44 | closed bool 45 | } 46 | 47 | type updateMesg struct { 48 | node* redisNode 49 | movedTime time.Time 50 | } 51 | 52 | // NewCluster create a new redis cluster client with specified options. 53 | func NewCluster(options *Options) (*Cluster, error) { 54 | cluster := &Cluster{ 55 | nodes: make(map[string]*redisNode), 56 | connTimeout: options.ConnTimeout, 57 | readTimeout: options.ReadTimeout, 58 | writeTimeout: options.WriteTimeout, 59 | keepAlive: options.KeepAlive, 60 | aliveTime: options.AliveTime, 61 | updateList: make(chan updateMesg), 62 | } 63 | 64 | for i := range options.StartNodes { 65 | node := &redisNode{ 66 | address: options.StartNodes[i], 67 | connTimeout: options.ConnTimeout, 68 | readTimeout: options.ReadTimeout, 69 | writeTimeout: options.WriteTimeout, 70 | keepAlive: options.KeepAlive, 71 | aliveTime: options.AliveTime, 72 | } 73 | 74 | err := cluster.update(node) 75 | if err != nil { 76 | continue 77 | } else { 78 | go cluster.handleUpdate() 79 | return cluster, nil 80 | } 81 | } 82 | 83 | return nil, fmt.Errorf("NewCluster: no valid node in %v", options.StartNodes) 84 | } 85 | 86 | // Do excute a redis command with random number arguments. First argument will 87 | // be used as key to hash to a slot, so it only supports a subset of redis 88 | // commands. 89 | /// 90 | // SUPPORTED: most commands of keys, strings, lists, sets, sorted sets, hashes. 91 | // NOT SUPPORTED: scripts, transactions, clusters. 92 | // 93 | // Particularly, MSET/MSETNX/MGET are supported using result aggregation. 94 | // To MSET/MSETNX, there's no atomicity gurantee that given keys are set at once. 95 | // It's possible that some keys are set, while others not. 96 | // 97 | // See README.md for more details. 98 | // See full redis command list: http://www.redis.io/commands 99 | func (cluster *Cluster) Do(cmd string, args ...interface{}) (interface{}, error) { 100 | if len(args) < 1 { 101 | return nil, fmt.Errorf("Do: no key found in args") 102 | } 103 | 104 | if cmd == "MSET" || cmd == "MSETNX" { 105 | return cluster.multiSet(cmd, args...) 106 | } 107 | 108 | if cmd == "MGET" { 109 | return cluster.multiGet(cmd, args...) 110 | } 111 | 112 | node, err := cluster.getNodeByKey(args[0]) 113 | if err != nil { 114 | return nil, fmt.Errorf("Do: %v", err) 115 | } 116 | 117 | reply, err := node.do(cmd, args...) 118 | if err != nil { 119 | return nil, fmt.Errorf("Do: %v", err) 120 | } 121 | 122 | resp := checkReply(reply) 123 | 124 | switch(resp) { 125 | case kRespOK, kRespError: 126 | return reply, nil 127 | case kRespMove: 128 | return cluster.handleMove(node, reply.(redisError).Error(), cmd, args) 129 | case kRespAsk: 130 | return cluster.handleAsk(node, reply.(redisError).Error(), cmd, args) 131 | case kRespConnTimeout: 132 | return cluster.handleConnTimeout(node, cmd, args) 133 | } 134 | 135 | panic("unreachable") 136 | } 137 | 138 | // Close cluster connection, any subsequent method call will fail. 139 | func (cluster *Cluster) Close() { 140 | cluster.rwLock.Lock() 141 | defer cluster.rwLock.Unlock() 142 | 143 | for addr, node := range cluster.nodes { 144 | node.shutdown() 145 | delete(cluster.nodes, addr) 146 | } 147 | 148 | cluster.closed = true 149 | } 150 | 151 | func (cluster *Cluster) handleMove(node *redisNode, replyMsg, cmd string, args []interface{}) (interface{}, error) { 152 | fields := strings.Split(replyMsg, " ") 153 | if len(fields) != 3 { 154 | return nil, fmt.Errorf("handleMove: invalid response \"%s\"", replyMsg) 155 | } 156 | 157 | // cluster has changed, inform update routine 158 | cluster.inform(node) 159 | 160 | newNode, err := cluster.getNodeByAddr(fields[2]) 161 | if err != nil { 162 | return nil, fmt.Errorf("handleMove: %v", err) 163 | } 164 | 165 | return newNode.do(cmd, args...) 166 | } 167 | 168 | func (cluster *Cluster) handleAsk(node *redisNode, replyMsg, cmd string, args []interface{}) (interface{}, error) { 169 | fields := strings.Split(replyMsg, " ") 170 | if len(fields) != 3 { 171 | return nil, fmt.Errorf("handleAsk: invalid response \"%s\"", replyMsg) 172 | } 173 | 174 | newNode, err := cluster.getNodeByAddr(fields[2]) 175 | if err != nil { 176 | return nil, fmt.Errorf("handleAsk: %v", err) 177 | } 178 | 179 | conn, err := newNode.getConn() 180 | if err != nil { 181 | return nil, fmt.Errorf("handleAsk: %v", err) 182 | } 183 | 184 | conn.send("ASKING") 185 | conn.send(cmd, args...) 186 | 187 | err = conn.flush() 188 | if err != nil { 189 | conn.shutdown() 190 | return nil, fmt.Errorf("handleAsk: %v", err) 191 | } 192 | 193 | re, err := String(conn.receive()) 194 | if err != nil || re != "OK" { 195 | conn.shutdown() 196 | return nil, fmt.Errorf("handleAsk: %v", err) 197 | } 198 | 199 | reply, err := conn.receive() 200 | if err != nil { 201 | conn.shutdown() 202 | return nil, fmt.Errorf("handleAsk: %v", err) 203 | } 204 | 205 | newNode.releaseConn(conn) 206 | 207 | return reply, nil 208 | } 209 | 210 | func (cluster *Cluster) handleConnTimeout(node *redisNode, cmd string, args []interface{}) (interface{}, error) { 211 | var randomNode *redisNode 212 | 213 | // choose a random node other than previous one 214 | cluster.rwLock.RLock() 215 | for _, randomNode = range cluster.nodes { 216 | if randomNode.address != node.address { 217 | break 218 | } 219 | } 220 | cluster.rwLock.RUnlock() 221 | 222 | reply, err := randomNode.do(cmd, args...) 223 | if err != nil { 224 | return nil, fmt.Errorf("handleConnTimeout: %v", err) 225 | } 226 | 227 | if _, ok := reply.(redisError); !ok { 228 | // we happen to choose the right node, which means 229 | // that cluster has changed, so inform update routine. 230 | cluster.inform(randomNode) 231 | return reply, nil 232 | } 233 | 234 | // ignore replies other than MOVED 235 | errMsg := reply.(redisError).Error() 236 | if len(errMsg) < 5 || string(errMsg[:5]) != "MOVED" { 237 | return nil, errors.New(errMsg) 238 | } 239 | 240 | // When MOVED received, we check wether move adress equal to 241 | // previous one. If equal, then it's just an connection timeout 242 | // error, return error and carry on. If not, then the master may 243 | // down or unreachable, a new master has served the slot, request 244 | // new master and update cluster info. 245 | // 246 | // TODO: At worst case, it will request redis 3 times on a single 247 | // command, will this be a problem? 248 | fields := strings.Split(errMsg, " ") 249 | if len(fields) != 3 { 250 | return nil, fmt.Errorf("handleConnTimeout: invalid response \"%s\"", errMsg) 251 | } 252 | 253 | if fields[2] == node.address { 254 | return nil, fmt.Errorf("handleConnTimeout: %s connection timeout", node.address) 255 | } 256 | 257 | // cluster change, inform back routine to update 258 | cluster.inform(randomNode) 259 | 260 | newNode, err := cluster.getNodeByAddr(fields[2]) 261 | if err != nil { 262 | return nil, fmt.Errorf("handleConnTimeout: %v", err) 263 | } 264 | 265 | return newNode.do(cmd, args...) 266 | } 267 | 268 | const ( 269 | kClusterSlots = 16384 270 | 271 | kRespOK = 0 272 | kRespMove = 1 273 | kRespAsk = 2 274 | kRespConnTimeout = 3 275 | kRespError = 4 276 | ) 277 | 278 | func checkReply(reply interface{}) int { 279 | if _, ok := reply.(redisError); !ok { 280 | return kRespOK 281 | } 282 | 283 | errMsg := reply.(redisError).Error() 284 | 285 | if len(errMsg) >= 3 && string(errMsg[:3]) == "ASK" { 286 | return kRespAsk 287 | } 288 | 289 | if len(errMsg) >= 5 && string(errMsg[:5]) == "MOVED" { 290 | return kRespMove 291 | } 292 | 293 | if len(errMsg) >= 12 && string(errMsg[:12]) == "ECONNTIMEOUT" { 294 | return kRespConnTimeout 295 | } 296 | 297 | return kRespError 298 | } 299 | 300 | func (cluster *Cluster) update(node *redisNode) error { 301 | info, err := Values(node.do("CLUSTER", "SLOTS")) 302 | if err != nil { 303 | return err 304 | } 305 | 306 | errFormat := fmt.Errorf("update: %s invalid response", node.address) 307 | 308 | var nslots int 309 | slots := make(map[string][]uint16) 310 | 311 | for _, i := range info { 312 | m, err := Values(i, err) 313 | if err != nil || len(m) < 3 { 314 | return errFormat 315 | } 316 | 317 | start, err := Int(m[0], err) 318 | if err != nil { 319 | return errFormat 320 | } 321 | 322 | end, err := Int(m[1], err) 323 | if err != nil { 324 | return errFormat 325 | } 326 | 327 | t, err := Values(m[2], err) 328 | if err != nil || len(t) < 2 { 329 | return errFormat 330 | } 331 | 332 | var ip string 333 | var port int 334 | 335 | _, err = Scan(t, &ip, &port) 336 | if err != nil { 337 | return errFormat 338 | } 339 | addr := fmt.Sprintf("%s:%d", ip, port) 340 | 341 | slot, ok := slots[addr] 342 | if !ok { 343 | slot = make([]uint16, 0, 2) 344 | } 345 | 346 | nslots += end - start + 1 347 | 348 | slot = append(slot, uint16(start)) 349 | slot = append(slot , uint16(end)) 350 | 351 | slots[addr] = slot 352 | } 353 | 354 | // TODO: Is full coverage really needed? 355 | if nslots != kClusterSlots { 356 | return fmt.Errorf("update: %s slots not full covered", node.address) 357 | } 358 | 359 | cluster.rwLock.Lock() 360 | defer cluster.rwLock.Unlock() 361 | 362 | t := time.Now() 363 | cluster.updateTime = t 364 | 365 | for addr, slot := range slots { 366 | node, ok := cluster.nodes[addr] 367 | if !ok { 368 | node = &redisNode { 369 | address: addr, 370 | connTimeout: cluster.connTimeout, 371 | readTimeout: cluster.readTimeout, 372 | writeTimeout: cluster.writeTimeout, 373 | keepAlive: cluster.keepAlive, 374 | aliveTime: cluster.aliveTime, 375 | } 376 | } 377 | 378 | n := len(slot) 379 | for i := 0; i < n - 1; i += 2 { 380 | start := slot[i] 381 | end := slot[i+1] 382 | 383 | for j := start; j <= end; j++ { 384 | cluster.slots[j] = node 385 | } 386 | } 387 | 388 | node.updateTime = t 389 | cluster.nodes[addr] = node 390 | } 391 | 392 | // shrink 393 | for addr, node := range cluster.nodes { 394 | if node.updateTime != t { 395 | node.shutdown() 396 | 397 | delete(cluster.nodes, addr) 398 | } 399 | } 400 | 401 | return nil 402 | } 403 | 404 | func (cluster *Cluster) handleUpdate() { 405 | for { 406 | msg := <-cluster.updateList 407 | 408 | // TODO: control update frequency by updateTime and movedTime? 409 | 410 | err := cluster.update(msg.node) 411 | if err != nil { 412 | log.Printf("handleUpdate: %v\n", err) 413 | } 414 | } 415 | } 416 | 417 | func (cluster *Cluster) inform(node *redisNode) { 418 | mesg := updateMesg { 419 | node: node, 420 | movedTime: time.Now(), 421 | } 422 | 423 | select { 424 | case cluster.updateList <- mesg: 425 | // Push update message, no more to do. 426 | default: 427 | // Update channel full, just carry on. 428 | } 429 | } 430 | 431 | func (cluster *Cluster) getNodeByAddr(addr string) (*redisNode, error) { 432 | cluster.rwLock.RLock() 433 | defer cluster.rwLock.RUnlock() 434 | 435 | if cluster.closed { 436 | return nil, fmt.Errorf("getNodeByAddr: cluster has been closed") 437 | } 438 | 439 | node, ok := cluster.nodes[addr] 440 | if !ok { 441 | return nil, fmt.Errorf("getNodeByAddr: %s not found", addr) 442 | } 443 | 444 | return node, nil 445 | } 446 | 447 | func (cluster *Cluster) getNodeByKey(arg interface{}) (*redisNode, error) { 448 | key, err := key(arg) 449 | if err != nil { 450 | return nil, fmt.Errorf("getNodeByKey: invalid key %v", key) 451 | } 452 | 453 | slot := hash(key) 454 | 455 | cluster.rwLock.RLock() 456 | defer cluster.rwLock.RUnlock() 457 | 458 | if cluster.closed { 459 | return nil, fmt.Errorf("getNodeByKey: cluster has been closed") 460 | } 461 | 462 | node := cluster.slots[slot] 463 | if node == nil { 464 | return nil, fmt.Errorf("getNodeByKey: %s[%d] no node found", key, slot) 465 | } 466 | 467 | return node, nil 468 | } 469 | 470 | func key(arg interface{}) (string, error) { 471 | switch arg := arg.(type) { 472 | case int: 473 | return strconv.Itoa(arg), nil 474 | case int64: 475 | return strconv.Itoa(int(arg)), nil 476 | case float64: 477 | return strconv.FormatFloat(arg, 'g', -1, 64), nil 478 | case string: 479 | return arg, nil 480 | case []byte: 481 | return string(arg), nil 482 | default: 483 | return "", fmt.Errorf("key: unknown type %T", arg) 484 | } 485 | } 486 | 487 | func hash(key string) uint16 { 488 | var s, e int 489 | for s = 0; s < len(key); s++ { 490 | if key[s] == '{' { 491 | break 492 | } 493 | } 494 | 495 | if s == len(key) { 496 | return crc16(key) & (kClusterSlots-1) 497 | } 498 | 499 | for e = s+1; e < len(key); e++ { 500 | if key[e] == '}' { 501 | break 502 | } 503 | } 504 | 505 | if e == len(key) || e == s+1 { 506 | return crc16(key) & (kClusterSlots-1) 507 | } 508 | 509 | return crc16(key[s+1:e]) & (kClusterSlots-1) 510 | } 511 | 512 | func init() { 513 | log.SetFlags(log.LstdFlags | log.Lshortfile) 514 | } 515 | -------------------------------------------------------------------------------- /crc16.go: -------------------------------------------------------------------------------- 1 | /* CRC16 implementation according to CCITT standards. 2 | * 3 | * Note by @antirez: this is actually the XMODEM CRC 16 algorithm, using the 4 | * following parameters: 5 | * 6 | * Name : "XMODEM", also known as "ZMODEM", "CRC-16/ACORN" 7 | * Width : 16 bit 8 | * Poly : 1021 (That is actually x^16 + x^12 + x^5 + 1) 9 | * Initialization : 0000 10 | * Reflect Input byte : False 11 | * Reflect Output CRC : False 12 | * Xor constant to output CRC : 0000 13 | * Output for "123456789" : 31C3 14 | */ 15 | 16 | package redis 17 | 18 | var crc16tab = [256]uint16{ 19 | 0x0000,0x1021,0x2042,0x3063,0x4084,0x50a5,0x60c6,0x70e7, 20 | 0x8108,0x9129,0xa14a,0xb16b,0xc18c,0xd1ad,0xe1ce,0xf1ef, 21 | 0x1231,0x0210,0x3273,0x2252,0x52b5,0x4294,0x72f7,0x62d6, 22 | 0x9339,0x8318,0xb37b,0xa35a,0xd3bd,0xc39c,0xf3ff,0xe3de, 23 | 0x2462,0x3443,0x0420,0x1401,0x64e6,0x74c7,0x44a4,0x5485, 24 | 0xa56a,0xb54b,0x8528,0x9509,0xe5ee,0xf5cf,0xc5ac,0xd58d, 25 | 0x3653,0x2672,0x1611,0x0630,0x76d7,0x66f6,0x5695,0x46b4, 26 | 0xb75b,0xa77a,0x9719,0x8738,0xf7df,0xe7fe,0xd79d,0xc7bc, 27 | 0x48c4,0x58e5,0x6886,0x78a7,0x0840,0x1861,0x2802,0x3823, 28 | 0xc9cc,0xd9ed,0xe98e,0xf9af,0x8948,0x9969,0xa90a,0xb92b, 29 | 0x5af5,0x4ad4,0x7ab7,0x6a96,0x1a71,0x0a50,0x3a33,0x2a12, 30 | 0xdbfd,0xcbdc,0xfbbf,0xeb9e,0x9b79,0x8b58,0xbb3b,0xab1a, 31 | 0x6ca6,0x7c87,0x4ce4,0x5cc5,0x2c22,0x3c03,0x0c60,0x1c41, 32 | 0xedae,0xfd8f,0xcdec,0xddcd,0xad2a,0xbd0b,0x8d68,0x9d49, 33 | 0x7e97,0x6eb6,0x5ed5,0x4ef4,0x3e13,0x2e32,0x1e51,0x0e70, 34 | 0xff9f,0xefbe,0xdfdd,0xcffc,0xbf1b,0xaf3a,0x9f59,0x8f78, 35 | 0x9188,0x81a9,0xb1ca,0xa1eb,0xd10c,0xc12d,0xf14e,0xe16f, 36 | 0x1080,0x00a1,0x30c2,0x20e3,0x5004,0x4025,0x7046,0x6067, 37 | 0x83b9,0x9398,0xa3fb,0xb3da,0xc33d,0xd31c,0xe37f,0xf35e, 38 | 0x02b1,0x1290,0x22f3,0x32d2,0x4235,0x5214,0x6277,0x7256, 39 | 0xb5ea,0xa5cb,0x95a8,0x8589,0xf56e,0xe54f,0xd52c,0xc50d, 40 | 0x34e2,0x24c3,0x14a0,0x0481,0x7466,0x6447,0x5424,0x4405, 41 | 0xa7db,0xb7fa,0x8799,0x97b8,0xe75f,0xf77e,0xc71d,0xd73c, 42 | 0x26d3,0x36f2,0x0691,0x16b0,0x6657,0x7676,0x4615,0x5634, 43 | 0xd94c,0xc96d,0xf90e,0xe92f,0x99c8,0x89e9,0xb98a,0xa9ab, 44 | 0x5844,0x4865,0x7806,0x6827,0x18c0,0x08e1,0x3882,0x28a3, 45 | 0xcb7d,0xdb5c,0xeb3f,0xfb1e,0x8bf9,0x9bd8,0xabbb,0xbb9a, 46 | 0x4a75,0x5a54,0x6a37,0x7a16,0x0af1,0x1ad0,0x2ab3,0x3a92, 47 | 0xfd2e,0xed0f,0xdd6c,0xcd4d,0xbdaa,0xad8b,0x9de8,0x8dc9, 48 | 0x7c26,0x6c07,0x5c64,0x4c45,0x3ca2,0x2c83,0x1ce0,0x0cc1, 49 | 0xef1f,0xff3e,0xcf5d,0xdf7c,0xaf9b,0xbfba,0x8fd9,0x9ff8, 50 | 0x6e17,0x7e36,0x4e55,0x5e74,0x2e93,0x3eb2,0x0ed1,0x1ef0, 51 | } 52 | 53 | func crc16(buf string) uint16 { 54 | var crc uint16 55 | for _, n := range buf { 56 | crc = (crc<>uint16(8)) ^ uint16(n))&0x00FF]; 57 | } 58 | return crc 59 | } 60 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package redis implement a pure redis cluster client, meaning it doesn't 3 | support any cluster commands. 4 | 5 | Create a new cluster client with specified options: 6 | 7 | cluster, err := redis.NewCluster( 8 | &redis.Options{ 9 | StartNodes: []string{"127.0.0.1:7000", "127.0.0.1:7001", "127.0.0.1:7002"}, 10 | ConnTimeout: 50 * time.Millisecond, 11 | ReadTimeout: 50 * time.Millisecond, 12 | WriteTimeout: 50 * time.Millisecond, 13 | KeepAlive: 16, 14 | AliveTime: 60 * time.Second, 15 | }) 16 | 17 | For basic usage: 18 | 19 | cluster.Do("SET", "foo", "bar") 20 | cluster.Do("INCR", "mycount", 1) 21 | cluster.Do("LPUSH", "mylist", "foo", "bar") 22 | cluster.Do("HMSET", "myhash", "f1", "foo", "f2", "bar") 23 | 24 | Use convert help functions to convert replies to int, float, string, etc: 25 | 26 | reply, err := Int(cluster.Do("INCR", "mycount", 1)) 27 | reply, err := String(cluster.Do("GET", "foo")) 28 | reply, err := Strings(cluster.Do("LRANGE", "mylist", 0, -1)) 29 | reply, err := StringMap(cluster.Do("HGETALL", "myhash")) 30 | 31 | Use batch interface to pack multiple commands for pipelining: 32 | 33 | batch := cluster.NewBatch() 34 | batch.Put("LPUSH", "country_list", "France") 35 | batch.Put("LPUSH", "country_list", "Italy") 36 | batch.Put("LPUSH", "country_list", "Germany") 37 | batch.Put("INCRBY", "countries", 3) 38 | batch.Put("LRANGE", "country_list", 0, -1) 39 | */ 40 | package redis 41 | -------------------------------------------------------------------------------- /example/example1.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "fmt" 6 | "time" 7 | "strconv" 8 | 9 | "github.com/gitstliu/go-redis-cluster" 10 | ) 11 | 12 | const kNumOfRoutine = 50 13 | 14 | func main() { 15 | cluster, err := redis.NewCluster( 16 | &redis.Options{ 17 | StartNodes: []string{"10.168.66.193:7001", "10.168.66.193:7000", "10.168.66.187:7002"}, 18 | ConnTimeout: 50 * time.Millisecond, 19 | ReadTimeout: 50 * time.Millisecond, 20 | WriteTimeout: 50 * time.Millisecond, 21 | KeepAlive: 16, 22 | AliveTime: 60 * time.Second, 23 | }) 24 | 25 | if err != nil { 26 | log.Fatalf("redis.New error: %s", err.Error()) 27 | } 28 | 29 | chann := make(chan int, kNumOfRoutine) 30 | for i := 0; i < kNumOfRoutine; i++ { 31 | go redisTest(cluster, i * 100000, (i+1)*100000, chann) 32 | } 33 | 34 | for i := 0; i < kNumOfRoutine; i++ { 35 | _ = <-chann 36 | } 37 | } 38 | 39 | func redisTest(cluster *redis.Cluster, begin, end int, done chan int) { 40 | prefix := "mykey" 41 | for i := begin; i < end; i++ { 42 | key := prefix + strconv.Itoa(i) 43 | 44 | _, err := cluster.Do("set", key, i*10) 45 | if err != nil { 46 | fmt.Printf("-set %s: %s\n", key, err.Error()) 47 | time.Sleep(100 * time.Millisecond) 48 | continue 49 | } 50 | value, err := redis.Int(cluster.Do("GET", key)) 51 | if err != nil { 52 | fmt.Printf("-get %s: %s\n", key, err.Error()) 53 | time.Sleep(100 * time.Millisecond) 54 | continue 55 | } 56 | if value != i*10 { 57 | fmt.Printf("-mismatch %s: %d\n", key, value) 58 | time.Sleep(100 * time.Millisecond) 59 | continue 60 | } 61 | fmt.Printf("+set %s\n", key) 62 | time.Sleep(50 * time.Millisecond) 63 | } 64 | 65 | done <- 1 66 | } 67 | -------------------------------------------------------------------------------- /example/example2.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/gitstliu/go-redis-cluster" 9 | ) 10 | 11 | func main() { 12 | cluster, err := redis.NewCluster( 13 | &redis.Options{ 14 | StartNodes: []string{"127.0.0.1:7000", "127.0.0.1:7001", "127.0.0.1:7002"}, 15 | ConnTimeout: 50 * time.Millisecond, 16 | ReadTimeout: 50 * time.Millisecond, 17 | WriteTimeout: 50 * time.Millisecond, 18 | KeepAlive: 16, 19 | AliveTime: 60 * time.Second, 20 | }) 21 | 22 | if err != nil { 23 | log.Fatalf("redis.New error: %s", err.Error()) 24 | } 25 | 26 | _, err = cluster.Do("set", "{user000}.name", "Joel") 27 | _, err = cluster.Do("set", "{user000}.age", "26") 28 | _, err = cluster.Do("set", "{user000}.country", "China") 29 | 30 | name, err := redis.String(cluster.Do("get", "{user000}.name")) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | age, err := redis.Int(cluster.Do("get", "{user000}.age")) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | country, err := redis.String(cluster.Do("get", "{user000}.country")) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | fmt.Printf("name: %s, age: %d, country: %s\n", name, age, country) 44 | 45 | cluster.Close() 46 | _, err = cluster.Do("set", "foo", "bar") 47 | if err == nil { 48 | log.Fatal("expect a none nil error") 49 | } 50 | log.Println(err) 51 | } 52 | -------------------------------------------------------------------------------- /example/example3.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/gitstliu/go-redis-cluster" 9 | ) 10 | 11 | func main() { 12 | cluster, err := redis.NewCluster( 13 | &redis.Options{ 14 | StartNodes: []string{"127.0.0.1:7000", "127.0.0.1:7001", "127.0.0.1:7002"}, 15 | ConnTimeout: 50 * time.Millisecond, 16 | ReadTimeout: 50 * time.Millisecond, 17 | WriteTimeout: 50 * time.Millisecond, 18 | KeepAlive: 16, 19 | AliveTime: 60 * time.Second, 20 | }) 21 | 22 | if err != nil { 23 | log.Fatalf("redis.New error: %s", err.Error()) 24 | } 25 | 26 | _, err = cluster.Do("MSET", "myfoo1", "mybar1", "myfoo2", "mybar2", "myfoo3", "mybar3") 27 | if err != nil { 28 | log.Fatalf("MSET error: %s", err.Error()) 29 | } 30 | 31 | values, err := redis.Strings(cluster.Do("MGET", "myfoo1", "myfoo5", "myfoo2", "myfoo3", "myfoo4")) 32 | if err != nil { 33 | log.Fatalf("MGET error: %s", err.Error()) 34 | } 35 | 36 | for i := range values { 37 | fmt.Printf("reply[%d]: %s\n", i, values[i]) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /example/example4.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/gitstliu/go-redis-cluster" 9 | ) 10 | 11 | func main() { 12 | cluster, err := redis.NewCluster( 13 | &redis.Options{ 14 | StartNodes: []string{"127.0.0.1:7000", "127.0.0.1:7001", "127.0.0.1:7002"}, 15 | ConnTimeout: 50 * time.Millisecond, 16 | ReadTimeout: 50 * time.Millisecond, 17 | WriteTimeout: 50 * time.Millisecond, 18 | KeepAlive: 16, 19 | AliveTime: 60 * time.Second, 20 | }) 21 | 22 | if err != nil { 23 | log.Fatalf("redis.New error: %s", err.Error()) 24 | } 25 | 26 | batch := cluster.NewBatch() 27 | batch.Put("INCR", "mycount") 28 | batch.Put("INCR", "mycount") 29 | batch.Put("INCR", "mycount") 30 | 31 | reply, err := cluster.RunBatch(batch) 32 | if err != nil { 33 | log.Fatalf("RunBatch error: %s", err.Error()) 34 | } 35 | 36 | for i := 0; i < 3; i++ { 37 | var resp int 38 | reply, err = redis.Scan(reply, &resp) 39 | if err != nil { 40 | log.Fatalf("RunBatch error: %s", err.Error()) 41 | } 42 | 43 | fmt.Printf("[%d] return: %d\n", i, resp) 44 | } 45 | 46 | batch = cluster.NewBatch() 47 | err = batch.Put("LPUSH", "country_list", "france") 48 | if err != nil { 49 | log.Fatalf("LPUSH error: %s", err.Error()) 50 | } 51 | err = batch.Put("LPUSH", "country_list", "italy") 52 | if err != nil { 53 | log.Fatalf("LPUSH error: %s", err.Error()) 54 | } 55 | err = batch.Put("LPUSH", "country_list", "germany") 56 | if err != nil { 57 | log.Fatalf("LPUSH error: %s", err.Error()) 58 | } 59 | err = batch.Put("INCRBY", "countries", 3) 60 | if err != nil { 61 | log.Fatalf("INCRBY error: %s", err.Error()) 62 | } 63 | err = batch.Put("LRANGE", "country_list", 0, -1) 64 | if err != nil { 65 | log.Fatalf("LRANGE error: %s", err.Error()) 66 | } 67 | 68 | reply, err = cluster.RunBatch(batch) 69 | if err != nil { 70 | log.Fatalf("RunBatch error: %s", err.Error()) 71 | } 72 | 73 | for i := 0; i < 4; i++ { 74 | var resp int 75 | reply, err = redis.Scan(reply, &resp) 76 | if err != nil { 77 | log.Fatalf("RunBatch error: %s", err.Error()) 78 | } 79 | 80 | fmt.Printf("[%d] return: %d\n", i, resp) 81 | } 82 | 83 | countries, err := redis.Strings(reply[0], nil) 84 | if err != nil { 85 | log.Fatalf("redis.Strings error: %s", err.Error()) 86 | } 87 | 88 | for i := range countries { 89 | fmt.Printf("[%d] %s\n", i, countries[i]) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /multi.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type multiTask struct { 8 | node *redisNode 9 | slot uint16 10 | 11 | cmd string 12 | args []interface{} 13 | 14 | reply interface{} 15 | replies []interface{} 16 | err error 17 | 18 | done chan int 19 | } 20 | 21 | func (cluster *Cluster) multiSet(cmd string, args ...interface{}) (interface{}, error) { 22 | if len(args) & 1 != 0 { 23 | return nil, fmt.Errorf("multiSet: invalid args %v", args) 24 | } 25 | 26 | tasks := make([]*multiTask, 0) 27 | 28 | cluster.rwLock.RLock() 29 | for i := 0; i < len(args); i += 2 { 30 | key, err := key(args[i]) 31 | if err != nil { 32 | cluster.rwLock.RUnlock() 33 | return nil, fmt.Errorf("multiSet: invalid key %v", args[i]) 34 | } 35 | 36 | slot := hash(key) 37 | 38 | var j int 39 | for j = 0; j < len(tasks); j++ { 40 | if tasks[j].slot == slot { 41 | tasks[j].args = append(tasks[j].args, args[i]) // key 42 | tasks[j].args = append(tasks[j].args, args[i+1]) // value 43 | 44 | break 45 | } 46 | } 47 | 48 | if j == len(tasks) { 49 | node := cluster.slots[slot] 50 | if node == nil { 51 | cluster.rwLock.RUnlock() 52 | return nil, fmt.Errorf("multiSet: %s[%d] no node found", key, slot) 53 | } 54 | 55 | task := &multiTask{ 56 | node: node, 57 | slot: slot, 58 | cmd: cmd, 59 | args: []interface{}{args[i], args[i+1]}, 60 | done: make(chan int), 61 | } 62 | tasks = append(tasks, task) 63 | } 64 | } 65 | cluster.rwLock.RUnlock() 66 | 67 | for i := range tasks { 68 | go handleSetTask(tasks[i]) 69 | } 70 | 71 | for i := range tasks { 72 | <-tasks[i].done 73 | } 74 | 75 | for i := range tasks { 76 | _, err := String(tasks[i].reply, tasks[i].err) 77 | if err != nil { 78 | return nil, err 79 | } 80 | } 81 | 82 | return "OK", nil 83 | } 84 | 85 | func (cluster *Cluster) multiGet(cmd string, args ...interface{}) (interface{}, error) { 86 | tasks := make([]*multiTask, 0) 87 | index := make([]*multiTask, len(args)) 88 | 89 | cluster.rwLock.RLock() 90 | for i := 0; i < len(args); i++ { 91 | key, err := key(args[i]) 92 | if err != nil { 93 | cluster.rwLock.RUnlock() 94 | return nil, fmt.Errorf("multiGet: invalid key %v", args[i]) 95 | } 96 | 97 | slot := hash(key) 98 | 99 | var j int 100 | for j = 0; j < len(tasks); j++ { 101 | if tasks[j].slot == slot { 102 | tasks[j].args = append(tasks[j].args, args[i]) // key 103 | index[i] = tasks[j] 104 | 105 | break 106 | } 107 | } 108 | 109 | if j == len(tasks) { 110 | node := cluster.slots[slot] 111 | if node == nil { 112 | cluster.rwLock.RUnlock() 113 | return nil, fmt.Errorf("multiGet: %s[%d] no node found", key, slot) 114 | } 115 | 116 | task := &multiTask{ 117 | node: node, 118 | slot: slot, 119 | cmd: cmd, 120 | args: []interface{}{args[i]}, 121 | done: make(chan int), 122 | } 123 | tasks = append(tasks, task) 124 | index[i] = tasks[j] 125 | } 126 | } 127 | cluster.rwLock.RUnlock() 128 | 129 | for i := range tasks { 130 | go handleGetTask(tasks[i]) 131 | } 132 | 133 | for i := range tasks { 134 | <-tasks[i].done 135 | } 136 | 137 | reply := make([]interface{}, len(args)) 138 | for i := range reply { 139 | if index[i].err != nil { 140 | return nil, index[i].err 141 | } 142 | 143 | if len(index[i].replies) < 0 { 144 | panic("unreachable") 145 | } 146 | 147 | reply[i] = index[i].replies[0] 148 | index[i].replies = index[i].replies[1:] 149 | } 150 | 151 | return reply, nil 152 | } 153 | 154 | func handleSetTask(task *multiTask) { 155 | task.reply, task.err = task.node.do(task.cmd, task.args...) 156 | task.done <- 1 157 | } 158 | 159 | func handleGetTask(task *multiTask) { 160 | task.replies, task.err = Values(task.node.do(task.cmd, task.args...)) 161 | task.done <- 1 162 | } 163 | -------------------------------------------------------------------------------- /node.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "bufio" 5 | "container/list" 6 | "errors" 7 | "fmt" 8 | "net" 9 | "strconv" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | type redisConn struct { 15 | c net.Conn 16 | t time.Time 17 | 18 | br *bufio.Reader 19 | bw *bufio.Writer 20 | 21 | readTimeout time.Duration 22 | writeTimeout time.Duration 23 | 24 | // Pending replies to be read in redis pipeling. 25 | pending int 26 | 27 | // Scratch space for formatting argument length. 28 | lenScratch [32]byte 29 | 30 | // Scratch space for formatting integer and float. 31 | numScratch [40]byte 32 | } 33 | 34 | type redisNode struct { 35 | address string 36 | 37 | conns list.List 38 | keepAlive int 39 | aliveTime time.Duration 40 | 41 | connTimeout time.Duration 42 | readTimeout time.Duration 43 | writeTimeout time.Duration 44 | 45 | mutex sync.Mutex 46 | 47 | updateTime time.Time 48 | 49 | closed bool 50 | } 51 | 52 | func (node *redisNode) getConn() (*redisConn, error) { 53 | node.mutex.Lock() 54 | 55 | if node.closed { 56 | node.mutex.Unlock() 57 | return nil, fmt.Errorf("getConn: connection has been closed") 58 | } 59 | 60 | // remove stale connections 61 | if node.connTimeout > 0 { 62 | for { 63 | elem := node.conns.Back() 64 | if elem == nil { 65 | break 66 | } 67 | conn := elem.Value.(*redisConn) 68 | if conn.t.Add(node.aliveTime).After(time.Now()) { 69 | break 70 | } 71 | node.conns.Remove(elem) 72 | } 73 | } 74 | 75 | if node.conns.Len() <= 0 { 76 | node.mutex.Unlock() 77 | 78 | c, err := net.DialTimeout("tcp", node.address, node.connTimeout) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | conn := &redisConn{ 84 | c: c, 85 | br: bufio.NewReader(c), 86 | bw: bufio.NewWriter(c), 87 | readTimeout: node.readTimeout, 88 | writeTimeout: node.writeTimeout, 89 | } 90 | 91 | return conn, nil 92 | } 93 | 94 | elem := node.conns.Back() 95 | node.conns.Remove(elem) 96 | node.mutex.Unlock() 97 | 98 | return elem.Value.(*redisConn), nil 99 | } 100 | 101 | func (node *redisNode) releaseConn(conn *redisConn) { 102 | node.mutex.Lock() 103 | defer node.mutex.Unlock() 104 | 105 | // Connection still has pending replies, just close it. 106 | if conn.pending > 0 || node.closed { 107 | conn.shutdown() 108 | return 109 | } 110 | 111 | if node.conns.Len() >= node.keepAlive || node.aliveTime <= 0 { 112 | conn.shutdown() 113 | return 114 | } 115 | 116 | conn.t = time.Now() 117 | node.conns.PushFront(conn) 118 | } 119 | 120 | func (conn *redisConn) shutdown() { 121 | conn.c.Close() 122 | } 123 | 124 | func (node *redisNode) shutdown() { 125 | node.mutex.Lock() 126 | defer node.mutex.Unlock() 127 | 128 | for { 129 | elem := node.conns.Back() 130 | if elem == nil { 131 | break 132 | } 133 | 134 | conn := elem.Value.(*redisConn) 135 | conn.c.Close() 136 | node.conns.Remove(elem) 137 | } 138 | 139 | node.closed = true 140 | } 141 | 142 | func (conn *redisConn) send(cmd string, args ...interface{}) error { 143 | conn.pending += 1 144 | 145 | if conn.writeTimeout > 0 { 146 | conn.c.SetWriteDeadline(time.Now().Add(conn.writeTimeout)) 147 | } 148 | 149 | if err := conn.writeCommand(cmd, args); err != nil { 150 | return err 151 | } 152 | 153 | return nil 154 | } 155 | 156 | func (conn *redisConn) flush() error { 157 | if conn.writeTimeout > 0 { 158 | conn.c.SetWriteDeadline(time.Now().Add(conn.writeTimeout)) 159 | } 160 | 161 | if err := conn.bw.Flush(); err != nil { 162 | return err 163 | } 164 | 165 | return nil 166 | } 167 | 168 | func (conn *redisConn) receive() (interface{}, error) { 169 | if conn.readTimeout > 0 { 170 | conn.c.SetWriteDeadline(time.Now().Add(conn.readTimeout)) 171 | } 172 | 173 | if conn.pending <= 0 { 174 | return nil, errors.New("no more pending reply") 175 | } 176 | 177 | conn.pending -= 1 178 | 179 | return conn.readReply() 180 | } 181 | 182 | func (node *redisNode) do(cmd string, args ...interface{}) (interface{}, error) { 183 | conn, err := node.getConn() 184 | if err != nil { 185 | return redisError("ECONNTIMEOUT"), nil 186 | } 187 | 188 | if err = conn.send(cmd, args...); err != nil { 189 | conn.shutdown() 190 | return nil, err 191 | } 192 | 193 | if err = conn.flush(); err != nil { 194 | conn.shutdown() 195 | return nil, err 196 | } 197 | 198 | reply, err := conn.receive() 199 | if err != nil { 200 | conn.shutdown() 201 | return nil, err 202 | } 203 | 204 | node.releaseConn(conn) 205 | 206 | return reply, err 207 | } 208 | 209 | func (conn *redisConn) writeLen(prefix byte, n int) error { 210 | conn.lenScratch[len(conn.lenScratch)-1] = '\n' 211 | conn.lenScratch[len(conn.lenScratch)-2] = '\r' 212 | i := len(conn.lenScratch) - 3 213 | 214 | for { 215 | conn.lenScratch[i] = byte('0' + n%10) 216 | i -= 1 217 | n = n / 10 218 | if n == 0 { 219 | break 220 | } 221 | } 222 | 223 | conn.lenScratch[i] = prefix 224 | _, err := conn.bw.Write(conn.lenScratch[i:]) 225 | 226 | return err 227 | } 228 | 229 | func (conn *redisConn) writeString(s string) error { 230 | conn.writeLen('$', len(s)) 231 | conn.bw.WriteString(s) 232 | _, err := conn.bw.WriteString("\r\n") 233 | 234 | return err 235 | } 236 | 237 | func (conn *redisConn) writeBytes(p []byte) error { 238 | conn.writeLen('$', len(p)) 239 | conn.bw.Write(p) 240 | _, err := conn.bw.WriteString("\r\n") 241 | 242 | return err 243 | } 244 | 245 | func (conn *redisConn) writeInt64(n int64) error { 246 | return conn.writeBytes(strconv.AppendInt(conn.numScratch[:0], n, 10)) 247 | } 248 | 249 | func (conn *redisConn) writeFloat64(n float64) error { 250 | return conn.writeBytes(strconv.AppendFloat(conn.numScratch[:0], n, 'g', -1, 64)) 251 | } 252 | 253 | // Args must be int64, float64, string, []byte, other types are not supported for safe reason. 254 | func (conn *redisConn) writeCommand(cmd string, args []interface{}) error { 255 | conn.writeLen('*', len(args)+1) 256 | err := conn.writeString(cmd) 257 | 258 | for _, arg := range args { 259 | if err != nil { 260 | break 261 | } 262 | switch arg := arg.(type) { 263 | case int: 264 | err = conn.writeInt64(int64(arg)) 265 | case int64: 266 | err = conn.writeInt64(arg) 267 | case float64: 268 | err = conn.writeFloat64(arg) 269 | case string: 270 | err = conn.writeString(arg) 271 | case []byte: 272 | err = conn.writeBytes(arg) 273 | default: 274 | err = fmt.Errorf("unknown type %T", arg) 275 | } 276 | } 277 | 278 | return err 279 | } 280 | 281 | // readLine read a single line terminated with CRLF. 282 | func (conn *redisConn) readLine(length int) ([]byte, error) { 283 | var line []byte 284 | for { 285 | p, err := conn.br.ReadBytes('\n') 286 | if err != nil { 287 | return nil, err 288 | } 289 | 290 | n := len(p) 291 | if length == 0 { 292 | n = n - 2 293 | } 294 | 295 | if n < 0 { 296 | return nil, errors.New("invalid response") 297 | } 298 | 299 | // bulk string may contain '\n', such as CLUSTER NODES 300 | if length == 0 { 301 | if p[n] != '\r' { 302 | if line != nil { 303 | line = append(line, p[:]...) 304 | } else { 305 | line = p 306 | } 307 | continue 308 | } 309 | } else { 310 | if line != nil { 311 | line = append(line, p[:]...) 312 | } else { 313 | line = p 314 | } 315 | } 316 | 317 | if len(line) < length { 318 | continue 319 | } else { 320 | 321 | if line != nil { 322 | result := append(line, p[:n]...) 323 | 324 | if length != 0 { 325 | return result[:length], nil 326 | } 327 | return result, nil 328 | } else { 329 | if length != 0 { 330 | return p[:length], nil 331 | } 332 | return p[:n], nil 333 | } 334 | } 335 | } 336 | } 337 | 338 | // parseLen parses bulk string and array length. 339 | func parseLen(p []byte) (int, error) { 340 | if len(p) == 0 { 341 | return -1, errors.New("invalid response") 342 | } 343 | 344 | // null element. 345 | if p[0] == '-' && len(p) == 2 && p[1] == '1' { 346 | return -1, nil 347 | } 348 | 349 | var n int 350 | for _, b := range p { 351 | n *= 10 352 | if b < '0' || b > '9' { 353 | return -1, errors.New("invalid response") 354 | } 355 | n += int(b - '0') 356 | } 357 | 358 | return n, nil 359 | } 360 | 361 | // parseInt parses an integer reply. 362 | func parseInt(p []byte) (int64, error) { 363 | if len(p) == 0 { 364 | return 0, errors.New("invalid response") 365 | } 366 | 367 | var negate bool 368 | if p[0] == '-' { 369 | negate = true 370 | p = p[1:] 371 | if len(p) == 0 { 372 | return 0, errors.New("invalid response") 373 | } 374 | } 375 | 376 | var n int64 377 | for _, b := range p { 378 | n *= 10 379 | if b < '0' || b > '9' { 380 | return 0, errors.New("invalid response") 381 | } 382 | n += int64(b - '0') 383 | } 384 | 385 | if negate { 386 | n = -n 387 | } 388 | 389 | return n, nil 390 | } 391 | 392 | var ( 393 | okReply interface{} = "OK" 394 | pongReply interface{} = "PONG" 395 | ) 396 | 397 | type redisError string 398 | 399 | func (err redisError) Error() string { return string(err) } 400 | 401 | func (conn *redisConn) readReply() (interface{}, error) { 402 | line, err := conn.readLine(0) 403 | if err != nil { 404 | return nil, err 405 | } 406 | if len(line) == 0 { 407 | return nil, errors.New("invalid reponse") 408 | } 409 | 410 | switch line[0] { 411 | case '+': 412 | switch { 413 | case len(line) == 3 && line[1] == 'O' && line[2] == 'K': 414 | // Avoid allocation for frequent "+OK" response. 415 | return okReply, nil 416 | case len(line) == 5 && line[1] == 'P' && line[2] == 'O' && line[3] == 'N' && line[4] == 'G': 417 | // Avoid allocation in PING command benchmarks :) 418 | return pongReply, nil 419 | default: 420 | return string(line[1:]), nil 421 | } 422 | case '-': 423 | return redisError(string(line[1:])), nil 424 | case ':': 425 | return parseInt(line[1:]) 426 | case '$': 427 | n, err := parseLen(line[1:]) 428 | if n < 0 || err != nil { 429 | return nil, err 430 | } 431 | 432 | line, err = conn.readLine(n) 433 | 434 | if err != nil { 435 | return nil, err 436 | } 437 | 438 | if len(line) != n { 439 | return nil, errors.New("invalid response") 440 | } 441 | 442 | return line, nil 443 | case '*': 444 | n, err := parseLen(line[1:]) 445 | if n < 0 || err != nil { 446 | return nil, err 447 | } 448 | 449 | r := make([]interface{}, n) 450 | for i := range r { 451 | r[i], err = conn.readReply() 452 | if err != nil { 453 | return nil, err 454 | } 455 | } 456 | 457 | return r, nil 458 | } 459 | 460 | return nil, errors.New("invalid response") 461 | } 462 | -------------------------------------------------------------------------------- /node_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "time" 5 | "testing" 6 | ) 7 | 8 | func TestRedisConn(t *testing.T) { 9 | node := newRedisNode() 10 | conn, err := node.getConn() 11 | if err != nil { 12 | t.Errorf("getConn error: %s\n", err.Error()) 13 | } 14 | node.releaseConn(conn) 15 | if node.conns.Len() != 1 { 16 | t.Errorf("releaseConn error") 17 | } 18 | 19 | conn1, err := node.getConn() 20 | if err != nil { 21 | t.Errorf("getConn error: %s\n", err.Error()) 22 | } 23 | if node.conns.Len() != 0 { 24 | t.Errorf("releaseConn error") 25 | } 26 | 27 | conn2, err := node.getConn() 28 | if err != nil { 29 | t.Errorf("getConn error: %s\n", err.Error()) 30 | } 31 | conn3, err := node.getConn() 32 | if err != nil { 33 | t.Errorf("getConn error: %s\n", err.Error()) 34 | } 35 | conn4, err := node.getConn() 36 | if err != nil { 37 | t.Errorf("getConn error: %s\n", err.Error()) 38 | } 39 | 40 | node.releaseConn(conn1) 41 | node.releaseConn(conn2) 42 | node.releaseConn(conn3) 43 | node.releaseConn(conn4) 44 | 45 | if node.conns.Len() != 3 { 46 | t.Errorf("releaseConn error") 47 | } 48 | 49 | conn, err = node.getConn() 50 | if err != nil { 51 | t.Errorf("getConn error: %s\n", err.Error()) 52 | } 53 | 54 | if node.conns.Len() != 2 { 55 | t.Errorf("releaseConn error") 56 | } 57 | } 58 | 59 | func TestRedisDo(t *testing.T) { 60 | node := newRedisNode() 61 | 62 | _, err := node.do("FLUSHALL") 63 | 64 | reply, err := node.do("SET", "foo", "bar") 65 | if err != nil { 66 | t.Errorf("SET error: %s\n", err.Error()) 67 | } 68 | if value, ok := reply.(string); !ok || value != "OK" { 69 | t.Errorf("unexpected value %v\n", reply) 70 | } 71 | 72 | reply, err = node.do("GET", "foo") 73 | if err != nil { 74 | t.Errorf("GET error: %s\n", err.Error()) 75 | } 76 | if value, ok := reply.([]byte); !ok || string(value) != "bar" { 77 | t.Errorf("unexpected value %v\n", reply) 78 | } 79 | 80 | reply, err = node.do("GET", "notexist") 81 | if err != nil { 82 | t.Errorf("GET error: %s\n", err.Error()) 83 | } 84 | if reply != nil { 85 | t.Errorf("unexpected value %v\n", reply) 86 | } 87 | 88 | reply, err = node.do("SETEX", "hello", 10, "world") 89 | if err != nil { 90 | t.Errorf("GET error: %s\n", err.Error()) 91 | } 92 | if value, ok := reply.(string); !ok || value != "OK" { 93 | t.Errorf("unexpected value %v\n", reply) 94 | } 95 | 96 | reply, err = node.do("INVALIDCOMMAND", "foo", "bar") 97 | if err != nil { 98 | t.Errorf("GET error: %s\n", err.Error()) 99 | } 100 | if _, ok := reply.(redisError); !ok { 101 | t.Errorf("unexpected value %v\n", reply) 102 | } 103 | 104 | reply, err = node.do("HGETALL", "foo") 105 | if err != nil { 106 | t.Errorf("GET error: %s\n", err.Error()) 107 | } 108 | if _, ok := reply.(redisError); !ok { 109 | t.Errorf("unexpected value %v\n", reply) 110 | } 111 | 112 | reply, err = node.do("HMSET", "myhash", "field1", "hello", "field2", "world") 113 | if err != nil { 114 | t.Errorf("GET error: %s\n", err.Error()) 115 | } 116 | if value, ok := reply.(string); !ok || value != "OK" { 117 | t.Errorf("unexpected value %v\n", reply) 118 | } 119 | 120 | reply, err = node.do("HSET", "myhash", "field3", "nice") 121 | if err != nil { 122 | t.Errorf("GET error: %s\n", err.Error()) 123 | } 124 | if value, ok := reply.(int64); !ok || value != 1 { 125 | t.Errorf("unexpected value %v\n", reply) 126 | } 127 | 128 | reply, err = node.do("HGETALL", "myhash") 129 | if err != nil { 130 | t.Errorf("GET error: %s\n", err.Error()) 131 | } 132 | if value, ok := reply.([]interface{}); !ok || len(value) != 6 { 133 | t.Errorf("unexpected value %v\n", reply) 134 | } 135 | } 136 | 137 | func TestRedisPipeline(t *testing.T) { 138 | node := newRedisNode() 139 | conn, err := node.getConn() 140 | if err != nil { 141 | t.Errorf("getConn error: %s\n", err.Error()) 142 | } 143 | 144 | err = conn.send("PING") 145 | if err != nil { 146 | t.Errorf("send error: %s\n", err.Error()) 147 | } 148 | err = conn.send("PING") 149 | if err != nil { 150 | t.Errorf("send error: %s\n", err.Error()) 151 | } 152 | err = conn.send("PING") 153 | if err != nil { 154 | t.Errorf("send error: %s\n", err.Error()) 155 | } 156 | 157 | err = conn.flush() 158 | if err != nil { 159 | t.Errorf("flush error: %s\n", err.Error()) 160 | } 161 | 162 | reply, err := String(conn.receive()) 163 | if err != nil { 164 | t.Errorf("flush error: %s\n", err.Error()) 165 | } 166 | if reply != "PONG" { 167 | t.Errorf("receive error: %s", reply) 168 | } 169 | reply, err = String(conn.receive()) 170 | if err != nil { 171 | t.Errorf("receive error: %s\n", err.Error()) 172 | } 173 | if reply != "PONG" { 174 | t.Errorf("receive error: %s", reply) 175 | } 176 | reply, err = String(conn.receive()) 177 | if err != nil { 178 | t.Errorf("receive error: %s\n", err.Error()) 179 | } 180 | if reply != "PONG" { 181 | t.Errorf("receive error: %s", reply) 182 | } 183 | reply, err = String(conn.receive()) 184 | if err == nil { 185 | t.Errorf("expect an error here") 186 | } 187 | if err.Error() != "no more pending reply" { 188 | t.Errorf("unexpected error: %s\n", err.Error()) 189 | } 190 | 191 | 192 | conn.send("SET", "mycount", 100) 193 | conn.send("INCR", "mycount") 194 | conn.send("INCRBY", "mycount", 20) 195 | conn.send("INCRBY", "mycount", 20) 196 | 197 | conn.flush() 198 | 199 | conn.receive() 200 | conn.receive() 201 | conn.receive() 202 | value, err := Int(conn.receive()) 203 | if value != 141 { 204 | t.Errorf("unexpected error: %d\n", reply) 205 | } 206 | } 207 | 208 | func newRedisNode() *redisNode { 209 | return &redisNode{ 210 | address: "127.0.0.1:6379", 211 | keepAlive: 3, 212 | aliveTime: 60 * time.Second, 213 | connTimeout: 50 * time.Millisecond, 214 | readTimeout: 50 * time.Millisecond, 215 | writeTimeout: 50 * time.Millisecond, 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /reply.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | ) 9 | 10 | // ErrNil indicates that a reply value is nil. 11 | var ErrNil = errors.New("nil reply") 12 | 13 | // Int is a helper that converts a command reply to an integer. If err is not 14 | // equal to nil, then Int returns 0, err. Otherwise, Int converts the 15 | // reply to an int as follows: 16 | // 17 | // Reply type Result 18 | // integer int(reply), nil 19 | // bulk string parsed reply, nil 20 | // nil 0, ErrNil 21 | // other 0, error 22 | func Int(reply interface{}, err error) (int, error) { 23 | if err != nil { 24 | return 0, err 25 | } 26 | switch reply := reply.(type) { 27 | case int64: 28 | x := int(reply) 29 | if int64(x) != reply { 30 | return 0, strconv.ErrRange 31 | } 32 | return x, nil 33 | case []byte: 34 | n, err := strconv.ParseInt(string(reply), 10, 0) 35 | return int(n), err 36 | case nil: 37 | return 0, ErrNil 38 | case redisError: 39 | return 0, reply 40 | } 41 | return 0, fmt.Errorf("unexpected type %T for Int", reply) 42 | } 43 | 44 | // Int64 is a helper that converts a command reply to 64 bit integer. If err is 45 | // not equal to nil, then Int returns 0, err. Otherwise, Int64 converts the 46 | // reply to an int64 as follows: 47 | // 48 | // Reply type Result 49 | // integer reply, nil 50 | // bulk string parsed reply, nil 51 | // nil 0, ErrNil 52 | // other 0, error 53 | func Int64(reply interface{}, err error) (int64, error) { 54 | if err != nil { 55 | return 0, err 56 | } 57 | switch reply := reply.(type) { 58 | case int64: 59 | return reply, nil 60 | case []byte: 61 | n, err := strconv.ParseInt(string(reply), 10, 64) 62 | return n, err 63 | case nil: 64 | return 0, ErrNil 65 | case redisError: 66 | return 0, reply 67 | } 68 | return 0, fmt.Errorf("unexpected type %T for Int64", reply) 69 | } 70 | 71 | // Float64 is a helper that converts a command reply to 64 bit float. If err is 72 | // not equal to nil, then Float64 returns 0, err. Otherwise, Float64 converts 73 | // the reply to an int as follows: 74 | // 75 | // Reply type Result 76 | // bulk string parsed reply, nil 77 | // nil 0, ErrNil 78 | // other 0, error 79 | func Float64(reply interface{}, err error) (float64, error) { 80 | if err != nil { 81 | return 0, err 82 | } 83 | switch reply := reply.(type) { 84 | case []byte: 85 | n, err := strconv.ParseFloat(string(reply), 64) 86 | return n, err 87 | case nil: 88 | return 0, ErrNil 89 | case redisError: 90 | return 0, reply 91 | } 92 | return 0, fmt.Errorf("unexpected type %T for Float64", reply) 93 | } 94 | 95 | // String is a helper that converts a command reply to a string. If err is not 96 | // equal to nil, then String returns "", err. Otherwise String converts the 97 | // reply to a string as follows: 98 | // 99 | // Reply type Result 100 | // bulk string string(reply), nil 101 | // simple string reply, nil 102 | // nil "", ErrNil 103 | // other "", error 104 | func String(reply interface{}, err error) (string, error) { 105 | if err != nil { 106 | return "", err 107 | } 108 | switch reply := reply.(type) { 109 | case []byte: 110 | return string(reply), nil 111 | case string: 112 | return reply, nil 113 | case nil: 114 | return "", ErrNil 115 | case redisError: 116 | return "", reply 117 | } 118 | return "", fmt.Errorf("unexpected type %T for String", reply) 119 | } 120 | 121 | // Bytes is a helper that converts a command reply to a slice of bytes. If err 122 | // is not equal to nil, then Bytes returns nil, err. Otherwise Bytes converts 123 | // the reply to a slice of bytes as follows: 124 | // 125 | // Reply type Result 126 | // bulk string reply, nil 127 | // simple string []byte(reply), nil 128 | // nil nil, ErrNil 129 | // other nil, error 130 | func Bytes(reply interface{}, err error) ([]byte, error) { 131 | if err != nil { 132 | return nil, err 133 | } 134 | switch reply := reply.(type) { 135 | case []byte: 136 | return reply, nil 137 | case string: 138 | return []byte(reply), nil 139 | case nil: 140 | return nil, ErrNil 141 | case redisError: 142 | return nil, reply 143 | } 144 | return nil, fmt.Errorf("unexpected type %T for Bytes", reply) 145 | } 146 | 147 | // Bool is a helper that converts a command reply to a boolean. If err is not 148 | // equal to nil, then Bool returns false, err. Otherwise Bool converts the 149 | // reply to boolean as follows: 150 | // 151 | // Reply type Result 152 | // integer value != 0, nil 153 | // bulk string strconv.ParseBool(reply) 154 | // nil false, ErrNil 155 | // other false, error 156 | func Bool(reply interface{}, err error) (bool, error) { 157 | if err != nil { 158 | return false, err 159 | } 160 | switch reply := reply.(type) { 161 | case int64: 162 | return reply != 0, nil 163 | case []byte: 164 | return strconv.ParseBool(string(reply)) 165 | case nil: 166 | return false, ErrNil 167 | case redisError: 168 | return false, reply 169 | } 170 | return false, fmt.Errorf("unexpected type %T for Bool", reply) 171 | } 172 | 173 | // Values is a helper that converts an array command reply to a []interface{}. 174 | // If err is not equal to nil, then Values returns nil, err. Otherwise, Values 175 | // converts the reply as follows: 176 | // 177 | // Reply type Result 178 | // array reply, nil 179 | // nil nil, ErrNil 180 | // other nil, error 181 | func Values(reply interface{}, err error) ([]interface{}, error) { 182 | if err != nil { 183 | return nil, err 184 | } 185 | switch reply := reply.(type) { 186 | case []interface{}: 187 | return reply, nil 188 | case nil: 189 | return nil, ErrNil 190 | case redisError: 191 | return nil, reply 192 | } 193 | return nil, fmt.Errorf("unexpected type %T for Values", reply) 194 | } 195 | 196 | // Ints is a helper that converts an array command reply to a []int. 197 | // If err is not equal to nil, then Ints returns nil, err. 198 | func Ints(reply interface{}, err error) ([]int, error) { 199 | values, err := Values(reply, err) 200 | if err != nil { 201 | return nil, err 202 | } 203 | 204 | ints := make([]int, len(values)) 205 | slice := make([]interface{}, len(values)) 206 | for i, _ := range ints { 207 | slice[i] = &ints[i] 208 | } 209 | 210 | if _, err = Scan(values, slice...); err != nil { 211 | return nil, err 212 | } 213 | 214 | return ints, nil 215 | } 216 | 217 | // Strings is a helper that converts an array command reply to a []string. If 218 | // err is not equal to nil, then Strings returns nil, err. Nil array items are 219 | // converted to "" in the output slice. Strings returns an error if an array 220 | // item is not a bulk string or nil. 221 | func Strings(reply interface{}, err error) ([]string, error) { 222 | values, err := Values(reply, err) 223 | if err != nil { 224 | return nil, err 225 | } 226 | 227 | strings := make([]string, len(values)) 228 | slice := make([]interface{}, len(values)) 229 | for i, _ := range strings { 230 | slice[i] = &strings[i] 231 | } 232 | 233 | if _, err = Scan(values, slice...); err != nil { 234 | return nil, err 235 | } 236 | 237 | return strings, nil 238 | } 239 | 240 | // StringMap is a helper that converts an array of strings (alternating key, value) 241 | // into a map[string]string. The HGETALL and CONFIG GET commands return replies in this format. 242 | // Requires an even number of values in result. 243 | func StringMap(result interface{}, err error) (map[string]string, error) { 244 | values, err := Values(result, err) 245 | if err != nil { 246 | return nil, err 247 | } 248 | if len(values)%2 != 0 { 249 | return nil, errors.New("expect even number elements for StringMap") 250 | } 251 | 252 | m := make(map[string]string, len(values)/2) 253 | for i := 0; i < len(values); i += 2 { 254 | key, okKey := values[i].([]byte) 255 | value, okValue := values[i+1].([]byte) 256 | if !okKey || !okValue { 257 | return nil, errors.New("expect bulk string for StringMap") 258 | } 259 | m[string(key)] = string(value) 260 | } 261 | 262 | return m, nil 263 | } 264 | 265 | // Scan copies from src to the values pointed at by dest. 266 | // 267 | // The values pointed at by dest must be an integer, float, boolean, string, 268 | // []byte, interface{} or slices of these types. Scan uses the standard strconv 269 | // package to convert bulk strings to numeric and boolean types. 270 | // 271 | // If a dest value is nil, then the corresponding src value is skipped. 272 | // 273 | // If a src element is nil, then the corresponding dest value is not modified. 274 | // 275 | // To enable easy use of Scan in a loop, Scan returns the slice of src 276 | // following the copied values. 277 | func Scan(src []interface{}, dst ...interface{}) ([]interface{}, error) { 278 | if len(src) < len(dst) { 279 | return nil, errors.New("mismatch length of source and dest") 280 | } 281 | var err error 282 | for i, d := range dst { 283 | err = convertAssign(d, src[i]) 284 | if err != nil { 285 | break 286 | } 287 | } 288 | return src[len(dst):], err 289 | } 290 | 291 | func ensureLen(d reflect.Value, n int) { 292 | if n > d.Cap() { 293 | d.Set(reflect.MakeSlice(d.Type(), n, n)) 294 | } else { 295 | d.SetLen(n) 296 | } 297 | } 298 | 299 | func cannotConvert(d reflect.Value, s interface{}) error { 300 | return fmt.Errorf("redigo: Scan cannot convert from %s to %s", 301 | reflect.TypeOf(s), d.Type()) 302 | } 303 | 304 | func convertAssignBytes(d reflect.Value, s []byte) (err error) { 305 | switch d.Type().Kind() { 306 | case reflect.Float32, reflect.Float64: 307 | var x float64 308 | x, err = strconv.ParseFloat(string(s), d.Type().Bits()) 309 | d.SetFloat(x) 310 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 311 | var x int64 312 | x, err = strconv.ParseInt(string(s), 10, d.Type().Bits()) 313 | d.SetInt(x) 314 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 315 | var x uint64 316 | x, err = strconv.ParseUint(string(s), 10, d.Type().Bits()) 317 | d.SetUint(x) 318 | case reflect.Bool: 319 | var x bool 320 | x, err = strconv.ParseBool(string(s)) 321 | d.SetBool(x) 322 | case reflect.String: 323 | d.SetString(string(s)) 324 | case reflect.Slice: 325 | if d.Type().Elem().Kind() != reflect.Uint8 { 326 | err = cannotConvert(d, s) 327 | } else { 328 | d.SetBytes(s) 329 | } 330 | default: 331 | err = cannotConvert(d, s) 332 | } 333 | return 334 | } 335 | 336 | func convertAssignInt(d reflect.Value, s int64) (err error) { 337 | switch d.Type().Kind() { 338 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 339 | d.SetInt(s) 340 | if d.Int() != s { 341 | err = strconv.ErrRange 342 | d.SetInt(0) 343 | } 344 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 345 | if s < 0 { 346 | err = strconv.ErrRange 347 | } else { 348 | x := uint64(s) 349 | d.SetUint(x) 350 | if d.Uint() != x { 351 | err = strconv.ErrRange 352 | d.SetUint(0) 353 | } 354 | } 355 | case reflect.Bool: 356 | d.SetBool(s != 0) 357 | default: 358 | err = cannotConvert(d, s) 359 | } 360 | return 361 | } 362 | 363 | func convertAssignValue(d reflect.Value, s interface{}) (err error) { 364 | switch s := s.(type) { 365 | case []byte: 366 | err = convertAssignBytes(d, s) 367 | case int64: 368 | err = convertAssignInt(d, s) 369 | default: 370 | err = cannotConvert(d, s) 371 | } 372 | return err 373 | } 374 | 375 | func convertAssignValues(d reflect.Value, s []interface{}) error { 376 | if d.Type().Kind() != reflect.Slice { 377 | return cannotConvert(d, s) 378 | } 379 | ensureLen(d, len(s)) 380 | for i := 0; i < len(s); i++ { 381 | if err := convertAssignValue(d.Index(i), s[i]); err != nil { 382 | return err 383 | } 384 | } 385 | return nil 386 | } 387 | 388 | func convertAssign(d interface{}, s interface{}) (err error) { 389 | // Handle the most common destination types using type switches and 390 | // fall back to reflection for all other types. 391 | switch s := s.(type) { 392 | case nil: 393 | // ingore 394 | case []byte: 395 | switch d := d.(type) { 396 | case *string: 397 | *d = string(s) 398 | case *int: 399 | *d, err = strconv.Atoi(string(s)) 400 | case *int64: 401 | *d, err = strconv.ParseInt(string(s), 10, 64) 402 | case *bool: 403 | *d, err = strconv.ParseBool(string(s)) 404 | case *[]byte: 405 | *d = s 406 | case *interface{}: 407 | *d = s 408 | case nil: 409 | // skip value 410 | default: 411 | if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr { 412 | err = cannotConvert(d, s) 413 | } else { 414 | err = convertAssignBytes(d.Elem(), s) 415 | } 416 | } 417 | case string: 418 | switch d := d.(type) { 419 | case *string: 420 | *d = s 421 | case *int: 422 | *d, err = strconv.Atoi(s) 423 | case *int64: 424 | *d, err = strconv.ParseInt(s, 10, 64) 425 | case *bool: 426 | *d, err = strconv.ParseBool(s) 427 | case *[]byte: 428 | *d = []byte(s) 429 | case *interface{}: 430 | *d = s 431 | case nil: 432 | // skip value 433 | default: 434 | if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr { 435 | err = cannotConvert(d, s) 436 | } else { 437 | err = convertAssignBytes(d.Elem(), []byte(s)) 438 | } 439 | } 440 | case int64: 441 | switch d := d.(type) { 442 | case *int: 443 | x := int(s) 444 | if int64(x) != s { 445 | err = strconv.ErrRange 446 | x = 0 447 | } 448 | *d = x 449 | case *int64: 450 | *d = s 451 | case *bool: 452 | *d = s != 0 453 | case *interface{}: 454 | *d = s 455 | case nil: 456 | // skip value 457 | default: 458 | if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr { 459 | fmt.Println(1) 460 | err = cannotConvert(d, s) 461 | 462 | } else { 463 | err = convertAssignInt(d.Elem(), s) 464 | } 465 | } 466 | case []interface{}: 467 | switch d := d.(type) { 468 | case *[]interface{}: 469 | *d = s 470 | case *interface{}: 471 | *d = s 472 | case nil: 473 | // skip value 474 | default: 475 | if d := reflect.ValueOf(d); d.Type().Kind() != reflect.Ptr { 476 | err = cannotConvert(d, s) 477 | } else { 478 | err = convertAssignValues(d.Elem(), s) 479 | } 480 | } 481 | case redisError: 482 | err = s 483 | default: 484 | err = cannotConvert(reflect.ValueOf(d), s) 485 | } 486 | return 487 | } 488 | -------------------------------------------------------------------------------- /reply_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "testing" 5 | "strconv" 6 | ) 7 | 8 | func TestRedisInt(t *testing.T) { 9 | node := newRedisNode() 10 | _, err := node.do("SET", "count", 10) 11 | if err != nil { 12 | t.Errorf("SET error: %s\n", err.Error()) 13 | } 14 | 15 | count, err := Int(node.do("INCR", "count")) 16 | if err != nil { 17 | t.Errorf("SET error: %s\n", err.Error()) 18 | } 19 | if count != 11 { 20 | t.Errorf("unexpected count %d", count) 21 | } 22 | 23 | count, err = Int(node.do("INCRBY", "count", 10)) 24 | if err != nil { 25 | t.Errorf("SET error: %s\n", err.Error()) 26 | } 27 | if count != 21 { 28 | t.Errorf("unexpected count %d", count) 29 | } 30 | 31 | _, err = node.do("SET", "count", "string string") 32 | if err != nil { 33 | t.Errorf("SET error: %s\n", err.Error()) 34 | } 35 | 36 | count, err = Int(node.do("GET", "count")) 37 | if err == nil { 38 | t.Errorf("expected an error") 39 | } 40 | } 41 | 42 | func TestRedisInt64(t *testing.T) { 43 | node := newRedisNode() 44 | 45 | count, err := Int64(node.do("LPUSH", "mylist", 10)) 46 | if err != nil { 47 | t.Errorf("SET error: %s\n", err.Error()) 48 | } 49 | if count != 1 { 50 | t.Errorf("unexpected count %d", count) 51 | } 52 | 53 | count, err = Int64(node.do("LPUSH", "mylist", -20)) 54 | if err != nil { 55 | t.Errorf("SET error: %s\n", err.Error()) 56 | } 57 | if count != 2 { 58 | t.Errorf("unexpected count %d", count) 59 | } 60 | 61 | value, err := Int64(node.do("RPOP", "mylist")) 62 | if err != nil { 63 | t.Errorf("SET error: %s\n", err.Error()) 64 | } 65 | if value != 10 { 66 | t.Errorf("unexpected count %d", count) 67 | } 68 | 69 | value, err = Int64(node.do("RPOP", "mylist")) 70 | if err != nil { 71 | t.Errorf("SET error: %s\n", err.Error()) 72 | } 73 | if value != -20 { 74 | t.Errorf("unexpected count %d", count) 75 | } 76 | } 77 | 78 | func TestRedisFloat64(t *testing.T) { 79 | node := newRedisNode() 80 | 81 | _, err := node.do("SET", "pi", 3.14) 82 | if err != nil { 83 | t.Errorf("SET error: %s\n", err.Error()) 84 | } 85 | 86 | pi, err := Float64(node.do("GET", "pi")) 87 | if err != nil { 88 | t.Errorf("SET error: %s\n", err.Error()) 89 | } 90 | if strconv.FormatFloat(pi, 'f', -1, 64) != "3.14" { 91 | t.Errorf("unexpected value: %f\n", pi) 92 | } 93 | } 94 | 95 | func TestRedisString(t *testing.T) { 96 | node := newRedisNode() 97 | 98 | _, err := node.do("SET", "hello", "hello world") 99 | if err != nil { 100 | t.Errorf("SET error: %s\n", err.Error()) 101 | } 102 | 103 | value, err := String(node.do("GET", "hello")) 104 | if err != nil { 105 | t.Errorf("SET error: %s\n", err.Error()) 106 | } 107 | if value != "hello world" { 108 | t.Errorf("unexpected value: %f\n", value) 109 | } 110 | } 111 | 112 | func TestRedisBytes(t *testing.T) { 113 | node := newRedisNode() 114 | 115 | _, err := node.do("SET", "mykey1", "hello world") 116 | if err != nil { 117 | t.Errorf("SET error: %s\n", err.Error()) 118 | } 119 | 120 | value, err := Bytes(node.do("GET", "mykey1")) 121 | if err != nil { 122 | t.Errorf("SET error: %s\n", err.Error()) 123 | } 124 | if string(value) != "hello world" { 125 | t.Errorf("unexpected value: %f\n", value) 126 | } 127 | } 128 | 129 | func TestRedisBool(t *testing.T) { 130 | node := newRedisNode() 131 | 132 | _, err := node.do("SET", "mykey2", "true") 133 | if err != nil { 134 | t.Errorf("SET error: %s\n", err.Error()) 135 | } 136 | 137 | value, err := Bool(node.do("GET", "mykey2")) 138 | if err != nil { 139 | t.Errorf("SET error: %s\n", err.Error()) 140 | } 141 | if value != true { 142 | t.Errorf("unexpected value: %t\n", value) 143 | } 144 | 145 | _, err = node.do("SET", "mykey2", 0) 146 | if err != nil { 147 | t.Errorf("SET error: %s\n", err.Error()) 148 | } 149 | 150 | value, err = Bool(node.do("GET", "mykey2")) 151 | if err != nil { 152 | t.Errorf("SET error: %s\n", err.Error()) 153 | } 154 | if value != false { 155 | t.Errorf("unexpected value: %t\n", value) 156 | } 157 | } 158 | 159 | func TestRedisInts(t *testing.T) { 160 | node := newRedisNode() 161 | 162 | _, err := node.do("MSET", "key1", 0, "key2", 1, "key3", 2) 163 | if err != nil { 164 | t.Errorf("SET error: %s\n", err.Error()) 165 | } 166 | value, err := Ints(node.do("MGET", "key1", "key2", "key3")) 167 | if err != nil { 168 | t.Errorf("SET error: %s\n", err.Error()) 169 | } 170 | if len(value) != 3 || value[0] != 0 || value[1] != 1 || value[2] != 2 { 171 | t.Errorf("unexpected value: %v\n", value) 172 | } 173 | } 174 | 175 | func TestRedisStrings(t *testing.T) { 176 | node := newRedisNode() 177 | 178 | _, err := node.do("LPUSH", "mylist2", "aaa") 179 | if err != nil { 180 | t.Errorf("SET error: %s\n", err.Error()) 181 | } 182 | _, err = node.do("LPUSH", "mylist2", "bbb") 183 | if err != nil { 184 | t.Errorf("SET error: %s\n", err.Error()) 185 | } 186 | _, err = node.do("LPUSH", "mylist2", "ccc") 187 | if err != nil { 188 | t.Errorf("SET error: %s\n", err.Error()) 189 | } 190 | 191 | value, err := Strings(node.do("LRANGE", "mylist2", 0, -1)) 192 | if err != nil { 193 | t.Errorf("SET error: %s\n", err.Error()) 194 | } 195 | if len(value) != 3 || string(value[0]) != "ccc" || string(value[1]) != "bbb" || string(value[2]) != "aaa" { 196 | t.Errorf("unexpected value: %v\n", value) 197 | } 198 | } 199 | 200 | func TestRedisStringMap(t *testing.T) { 201 | node := newRedisNode() 202 | 203 | _, err := node.do("HMSET", "hashkey1", "name", "joel", "age", "26", "gender", "male") 204 | if err != nil { 205 | t.Errorf("HMSET error: %s\n", err.Error()) 206 | } 207 | 208 | value, err := StringMap(node.do("HGETALL", "hashkey1")) 209 | if err != nil { 210 | t.Errorf("HGETALL error: %s\n", err.Error()) 211 | } 212 | if v, ok := value["name"]; !ok || v != "joel" { 213 | t.Errorf("unexpected value: %v\n", value) 214 | } 215 | if v, ok := value["age"]; !ok || v != "26" { 216 | t.Errorf("unexpected value: %v\n", value) 217 | } 218 | if v, ok := value["gender"]; !ok || v != "male" { 219 | t.Errorf("unexpected value: %v\n", value) 220 | } 221 | } 222 | 223 | func TestRedisScan(t *testing.T) { 224 | node := newRedisNode() 225 | 226 | kv := []string{"key1", "hello", "key2", "-1024", "key3", "false", "key4", "-3.14"} 227 | ikv := make([]interface{}, len(kv)) 228 | for i, _ := range ikv { 229 | ikv[i] = kv[i] 230 | } 231 | _, err := node.do("MSET", ikv...) 232 | if err != nil { 233 | t.Errorf("MSET error: %s\n", err.Error()) 234 | } 235 | 236 | keys := []string{"key1", "key2", "key3", "key4"} 237 | ikeys := make([]interface{}, len(keys)) 238 | for i, _ := range ikeys { 239 | ikeys[i] = keys[i] 240 | } 241 | reply, err := Values(node.do("MGET", ikeys...)) 242 | if err != nil { 243 | t.Errorf("MSET error: %s\n", err.Error()) 244 | } 245 | 246 | var stringVal string 247 | var intVal int 248 | var boolVal bool 249 | var floatVal float64 250 | 251 | if reply, err = Scan(reply, &stringVal); err != nil{ 252 | t.Errorf("Scan error: %s\n", err.Error()) 253 | } 254 | if stringVal != "hello" { 255 | t.Errorf("unexpected value: %s\n", stringVal) 256 | } 257 | 258 | if reply, err = Scan(reply, &intVal); err != nil{ 259 | t.Errorf("Scan error: %s\n", err.Error()) 260 | } 261 | if intVal != -1024 { 262 | t.Errorf("unexpected value: %s\n", stringVal) 263 | } 264 | 265 | if reply, err = Scan(reply, &intVal); err == nil{ 266 | t.Errorf("expected an error") 267 | } 268 | 269 | if reply, err = Scan(reply, &floatVal); err != nil{ 270 | t.Errorf("Scan error: %s\n", err.Error()) 271 | } 272 | 273 | reply, err = Values(node.do("MGET", ikeys...)) 274 | if err != nil { 275 | t.Errorf("MSET error: %s\n", err.Error()) 276 | } 277 | 278 | if reply, err = Scan(reply, &stringVal, &intVal, &boolVal, &floatVal); err != nil{ 279 | t.Errorf("Scan error: %s\n", err.Error()) 280 | } 281 | if stringVal != "hello" && intVal != -1024 && boolVal != false { 282 | t.Errorf("unexpected value: %s %d %t %f\n", stringVal, intVal, boolVal, floatVal) 283 | } 284 | } 285 | --------------------------------------------------------------------------------