]>
Commit | Line | Data |
---|---|---|
4a327b4a | 1 | # RubyRedis is an alternative implementatin of Ruby client library written |
2 | # by Salvatore Sanfilippo. | |
3 | # | |
4 | # The aim of this library is to create an alternative client library that is | |
5 | # much simpler and does not implement every command explicitly but uses | |
6 | # method_missing instead. | |
7 | ||
8 | require 'socket' | |
e3d48807 | 9 | require 'set' |
4a327b4a | 10 | |
0dd8fce1 | 11 | begin |
06374c4b | 12 | if (RUBY_VERSION >= '1.9') |
13 | require 'timeout' | |
14 | RedisTimer = Timeout | |
15 | else | |
16 | require 'system_timer' | |
17 | RedisTimer = SystemTimer | |
18 | end | |
0dd8fce1 | 19 | rescue LoadError |
06374c4b | 20 | RedisTimer = nil |
0dd8fce1 | 21 | end |
22 | ||
4a327b4a | 23 | class RedisClient |
24 | BulkCommands = { | |
25 | "set"=>true, "setnx"=>true, "rpush"=>true, "lpush"=>true, "lset"=>true, | |
26 | "lrem"=>true, "sadd"=>true, "srem"=>true, "sismember"=>true, | |
27 | "echo"=>true, "getset"=>true, "smove"=>true | |
28 | } | |
29 | ||
3ba37089 | 30 | ConvertToBool = lambda{|r| r == 0 ? false : r} |
31 | ||
32 | ReplyProcessor = { | |
33 | "exists" => ConvertToBool, | |
34 | "sismember"=> ConvertToBool, | |
35 | "sadd"=> ConvertToBool, | |
36 | "srem"=> ConvertToBool, | |
37 | "smove"=> ConvertToBool, | |
38 | "move"=> ConvertToBool, | |
39 | "setnx"=> ConvertToBool, | |
40 | "del"=> ConvertToBool, | |
41 | "renamenx"=> ConvertToBool, | |
42 | "expire"=> ConvertToBool, | |
43 | "keys" => lambda{|r| r.split(" ")}, | |
44 | "info" => lambda{|r| | |
45 | info = {} | |
46 | r.each_line {|kv| | |
f5bf7e3e | 47 | k,v = kv.split(":",2).map{|x| x.chomp} |
3ba37089 | 48 | info[k.to_sym] = v |
49 | } | |
50 | info | |
51 | } | |
52 | } | |
53 | ||
0dd8fce1 | 54 | Aliases = { |
55 | "flush_db" => "flushdb", | |
56 | "flush_all" => "flushall", | |
57 | "last_save" => "lastsave", | |
58 | "key?" => "exists", | |
59 | "delete" => "del", | |
60 | "randkey" => "randomkey", | |
61 | "list_length" => "llen", | |
0dd8fce1 | 62 | "push_tail" => "rpush", |
63 | "push_head" => "lpush", | |
64 | "pop_tail" => "rpop", | |
65 | "pop_head" => "lpop", | |
66 | "list_set" => "lset", | |
67 | "list_range" => "lrange", | |
68 | "list_trim" => "ltrim", | |
69 | "list_index" => "lindex", | |
70 | "list_rm" => "lrem", | |
71 | "set_add" => "sadd", | |
72 | "set_delete" => "srem", | |
73 | "set_count" => "scard", | |
74 | "set_member?" => "sismember", | |
75 | "set_members" => "smembers", | |
76 | "set_intersect" => "sinter", | |
e3d48807 | 77 | "set_intersect_store" => "sinterstore", |
0dd8fce1 | 78 | "set_inter_store" => "sinterstore", |
79 | "set_union" => "sunion", | |
80 | "set_union_store" => "sunionstore", | |
81 | "set_diff" => "sdiff", | |
82 | "set_diff_store" => "sdiffstore", | |
83 | "set_move" => "smove", | |
e3d48807 | 84 | "set_unless_exists" => "setnx", |
85 | "rename_unless_exists" => "renamenx" | |
0dd8fce1 | 86 | } |
87 | ||
4a327b4a | 88 | def initialize(opts={}) |
0dd8fce1 | 89 | @host = opts[:host] || '127.0.0.1' |
90 | @port = opts[:port] || 6379 | |
91 | @db = opts[:db] || 0 | |
92 | @timeout = opts[:timeout] || 0 | |
3f32f1f6 | 93 | connect_to_server |
4a327b4a | 94 | end |
95 | ||
96 | def to_s | |
97 | "Redis Client connected to #{@host}:#{@port} against DB #{@db}" | |
98 | end | |
99 | ||
100 | def connect_to_server | |
0dd8fce1 | 101 | @sock = connect_to(@host,@port,@timeout == 0 ? nil : @timeout) |
3f32f1f6 | 102 | call_command(["select",@db]) if @db != 0 |
4a327b4a | 103 | end |
104 | ||
0dd8fce1 | 105 | def connect_to(host, port, timeout=nil) |
106 | # We support connect() timeout only if system_timer is availabe | |
107 | # or if we are running against Ruby >= 1.9 | |
108 | # Timeout reading from the socket instead will be supported anyway. | |
109 | if @timeout != 0 and RedisTimer | |
110 | begin | |
111 | sock = TCPSocket.new(host, port, 0) | |
112 | rescue Timeout::Error | |
e3d48807 | 113 | @sock = nil |
0dd8fce1 | 114 | raise Timeout::Error, "Timeout connecting to the server" |
115 | end | |
116 | else | |
117 | sock = TCPSocket.new(host, port, 0) | |
118 | end | |
a56785f7 | 119 | sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1 |
0dd8fce1 | 120 | |
121 | # If the timeout is set we set the low level socket options in order | |
122 | # to make sure a blocking read will return after the specified number | |
123 | # of seconds. This hack is from memcached ruby client. | |
124 | if timeout | |
125 | secs = Integer(timeout) | |
126 | usecs = Integer((timeout - secs) * 1_000_000) | |
127 | optval = [secs, usecs].pack("l_2") | |
128 | sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval | |
129 | sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval | |
130 | end | |
131 | sock | |
132 | end | |
133 | ||
4a327b4a | 134 | def method_missing(*argv) |
135 | call_command(argv) | |
136 | end | |
137 | ||
138 | def call_command(argv) | |
3f32f1f6 | 139 | # this wrapper to raw_call_command handle reconnection on socket |
140 | # error. We try to reconnect just one time, otherwise let the error | |
141 | # araise. | |
0dd8fce1 | 142 | connect_to_server if !@sock |
3f32f1f6 | 143 | begin |
144 | raw_call_command(argv) | |
145 | rescue Errno::ECONNRESET | |
146 | @sock.close | |
147 | connect_to_server | |
148 | raw_call_command(argv) | |
149 | end | |
150 | end | |
151 | ||
152 | def raw_call_command(argv) | |
4a327b4a | 153 | bulk = nil |
154 | argv[0] = argv[0].to_s.downcase | |
0dd8fce1 | 155 | argv[0] = Aliases[argv[0]] if Aliases[argv[0]] |
0b420168 | 156 | if BulkCommands[argv[0]] and argv.length > 1 |
3ba37089 | 157 | bulk = argv[-1].to_s |
4a327b4a | 158 | argv[-1] = bulk.length |
159 | end | |
160 | @sock.write(argv.join(" ")+"\r\n") | |
161 | @sock.write(bulk+"\r\n") if bulk | |
3ba37089 | 162 | |
163 | # Post process the reply if needed | |
164 | processor = ReplyProcessor[argv[0]] | |
165 | processor ? processor.call(read_reply) : read_reply | |
4a327b4a | 166 | end |
167 | ||
4e1684df | 168 | def select(*args) |
169 | raise "SELECT not allowed, use the :db option when creating the object" | |
170 | end | |
171 | ||
ad0ea27c | 172 | def [](key) |
173 | get(key) | |
174 | end | |
175 | ||
176 | def []=(key,value) | |
177 | set(key,value) | |
178 | end | |
179 | ||
0dd8fce1 | 180 | def sort(key, opts={}) |
181 | cmd = [] | |
182 | cmd << "SORT #{key}" | |
183 | cmd << "BY #{opts[:by]}" if opts[:by] | |
184 | cmd << "GET #{[opts[:get]].flatten * ' GET '}" if opts[:get] | |
185 | cmd << "#{opts[:order]}" if opts[:order] | |
186 | cmd << "LIMIT #{opts[:limit].join(' ')}" if opts[:limit] | |
187 | call_command(cmd) | |
188 | end | |
189 | ||
190 | def incr(key,increment=nil) | |
191 | call_command(increment ? ["incrby",key,increment] : ["incr",key]) | |
192 | end | |
193 | ||
194 | def decr(key,decrement=nil) | |
195 | call_command(decrement ? ["decrby",key,decrement] : ["decr",key]) | |
196 | end | |
197 | ||
4a327b4a | 198 | def read_reply |
0dd8fce1 | 199 | # We read the first byte using read() mainly because gets() is |
200 | # immune to raw socket timeouts. | |
201 | begin | |
202 | rtype = @sock.read(1) | |
203 | rescue Errno::EAGAIN | |
204 | # We want to make sure it reconnects on the next command after the | |
205 | # timeout. Otherwise the server may reply in the meantime leaving | |
206 | # the protocol in a desync status. | |
207 | @sock = nil | |
208 | raise Errno::EAGAIN, "Timeout reading from the socket" | |
209 | end | |
210 | ||
211 | raise Errno::ECONNRESET,"Connection lost" if !rtype | |
4a327b4a | 212 | line = @sock.gets |
0dd8fce1 | 213 | case rtype |
4a327b4a | 214 | when "-" |
0dd8fce1 | 215 | raise "-"+line.strip |
4a327b4a | 216 | when "+" |
0dd8fce1 | 217 | line.strip |
4a327b4a | 218 | when ":" |
0dd8fce1 | 219 | line.to_i |
4a327b4a | 220 | when "$" |
0dd8fce1 | 221 | bulklen = line.to_i |
4a327b4a | 222 | return nil if bulklen == -1 |
223 | data = @sock.read(bulklen) | |
224 | @sock.read(2) # CRLF | |
225 | data | |
226 | when "*" | |
0dd8fce1 | 227 | objects = line.to_i |
4a327b4a | 228 | return nil if bulklen == -1 |
229 | res = [] | |
230 | objects.times { | |
231 | res << read_reply | |
232 | } | |
233 | res | |
0dd8fce1 | 234 | else |
03fd01c7 | 235 | raise "Protocol error, got '#{rtype}' as initial reply byte" |
4a327b4a | 236 | end |
237 | end | |
238 | end |