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