redis 1.0.7 → 2.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,470 +1,78 @@
1
+ require "thread"
2
+
1
3
  class Redis
2
4
  class Client
3
- class ProtocolError < RuntimeError
4
- def initialize(reply_type)
5
- super("Protocol error, got '#{reply_type}' as initial reply byte")
6
- end
7
- end
8
-
9
- OK = "OK".freeze
10
5
  MINUS = "-".freeze
11
6
  PLUS = "+".freeze
12
7
  COLON = ":".freeze
13
8
  DOLLAR = "$".freeze
14
9
  ASTERISK = "*".freeze
15
10
 
16
- BULK_COMMANDS = {
17
- "set" => true,
18
- "setnx" => true,
19
- "rpush" => true,
20
- "lpush" => true,
21
- "lset" => true,
22
- "lrem" => true,
23
- "sadd" => true,
24
- "srem" => true,
25
- "sismember" => true,
26
- "echo" => true,
27
- "getset" => true,
28
- "smove" => true,
29
- "zadd" => true,
30
- "zincrby" => true,
31
- "zrem" => true,
32
- "zscore" => true,
33
- "zrank" => true,
34
- "zrevrank" => true,
35
- "hget" => true,
36
- "hdel" => true,
37
- "hexists" => true,
38
- "publish" => true
39
- }
40
-
41
- MULTI_BULK_COMMANDS = {
42
- "mset" => true,
43
- "msetnx" => true,
44
- "hset" => true,
45
- "hmset" => true
46
- }
47
-
48
- BOOLEAN_PROCESSOR = lambda{|r| r == 1 }
49
-
50
- REPLY_PROCESSOR = {
51
- "exists" => BOOLEAN_PROCESSOR,
52
- "sismember" => BOOLEAN_PROCESSOR,
53
- "sadd" => BOOLEAN_PROCESSOR,
54
- "srem" => BOOLEAN_PROCESSOR,
55
- "smove" => BOOLEAN_PROCESSOR,
56
- "zadd" => BOOLEAN_PROCESSOR,
57
- "zrem" => BOOLEAN_PROCESSOR,
58
- "move" => BOOLEAN_PROCESSOR,
59
- "setnx" => BOOLEAN_PROCESSOR,
60
- "del" => BOOLEAN_PROCESSOR,
61
- "renamenx" => BOOLEAN_PROCESSOR,
62
- "expire" => BOOLEAN_PROCESSOR,
63
- "hset" => BOOLEAN_PROCESSOR,
64
- "hexists" => BOOLEAN_PROCESSOR,
65
- "info" => lambda{|r|
66
- info = Hash.new do |hash, key|
67
- if hash.include?(key.to_s)
68
- Redis.deprecate "Redis#info will return a hash of string keys, not symbols", caller[2]
69
- hash[key.to_s]
70
- end
71
- end
72
-
73
- r.each_line {|kv|
74
- k,v = kv.split(":",2).map{|x| x.chomp}
75
- info[k] = v
76
- }
77
- info
78
- },
79
- "keys" => lambda{|r|
80
- if r.is_a?(Array)
81
- r
82
- else
83
- r.split(" ")
84
- end
85
- },
86
- "hgetall" => lambda{|r|
87
- Hash[*r]
88
- }
89
- }
90
-
91
- ALIASES = {
92
- "flush_db" => "flushdb",
93
- "flush_all" => "flushall",
94
- "last_save" => "lastsave",
95
- "key?" => "exists",
96
- "delete" => "del",
97
- "randkey" => "randomkey",
98
- "list_length" => "llen",
99
- "push_tail" => "rpush",
100
- "push_head" => "lpush",
101
- "pop_tail" => "rpop",
102
- "pop_head" => "lpop",
103
- "list_set" => "lset",
104
- "list_range" => "lrange",
105
- "list_trim" => "ltrim",
106
- "list_index" => "lindex",
107
- "list_rm" => "lrem",
108
- "set_add" => "sadd",
109
- "set_delete" => "srem",
110
- "set_count" => "scard",
111
- "set_member?" => "sismember",
112
- "set_members" => "smembers",
113
- "set_intersect" => "sinter",
114
- "set_intersect_store" => "sinterstore",
115
- "set_inter_store" => "sinterstore",
116
- "set_union" => "sunion",
117
- "set_union_store" => "sunionstore",
118
- "set_diff" => "sdiff",
119
- "set_diff_store" => "sdiffstore",
120
- "set_move" => "smove",
121
- "set_unless_exists" => "setnx",
122
- "rename_unless_exists" => "renamenx",
123
- "type?" => "type",
124
- "zset_add" => "zadd",
125
- "zset_count" => "zcard",
126
- "zset_range_by_score" => "zrangebyscore",
127
- "zset_reverse_range" => "zrevrange",
128
- "zset_range" => "zrange",
129
- "zset_delete" => "zrem",
130
- "zset_score" => "zscore",
131
- "zset_incr_by" => "zincrby",
132
- "zset_increment_by" => "zincrby"
133
- }
134
-
135
- DISABLED_COMMANDS = {
136
- "monitor" => true,
137
- "sync" => true
138
- }
139
-
140
- BLOCKING_COMMANDS = {
141
- "blpop" => true,
142
- "brpop" => true
143
- }
11
+ attr_accessor :db, :host, :port, :password, :timeout, :logger
144
12
 
145
13
  def initialize(options = {})
146
- @host = options[:host] || '127.0.0.1'
147
- @port = (options[:port] || 6379).to_i
148
- @db = (options[:db] || 0).to_i
14
+ @host = options[:host] || "127.0.0.1"
15
+ @port = (options[:port] || 6379).to_i
16
+ @db = (options[:db] || 0).to_i
149
17
  @timeout = (options[:timeout] || 5).to_i
150
18
  @password = options[:password]
151
19
  @logger = options[:logger]
152
- @thread_safe = options[:thread_safe]
153
- @binary_keys = options[:binary_keys]
154
- @mutex = Mutex.new if @thread_safe
20
+ @mutex = ::Mutex.new
155
21
  @sock = nil
156
- @pubsub = false
157
-
158
- log(self)
159
- end
160
-
161
- def to_s
162
- "Redis Client connected to #{server} against DB #{@db}"
163
- end
164
-
165
- def select(*args)
166
- raise "SELECT not allowed, use the :db option when creating the object"
167
- end
168
-
169
- def [](key)
170
- get(key)
171
- end
172
-
173
- def []=(key,value)
174
- set(key, value)
175
- end
176
-
177
- def get(key)
178
- call_command([:get, key])
179
- end
180
-
181
- def set(key, value, ttl = nil)
182
- if ttl
183
- deprecated("set with an expire", :set_with_expire, caller[0])
184
- set_with_expire(key, value, ttl)
185
- else
186
- call_command([:set, key, value])
187
- end
188
- end
189
-
190
- def set_with_expire(key, value, ttl)
191
- Redis.deprecate "Using a non-atomic set with expire. Use setex if your Redis version allows it.", caller[0]
192
- set(key, value)
193
- expire(key, ttl)
194
22
  end
195
23
 
196
- def mset(*args)
197
- if args.size == 1
198
- deprecated("mset with a hash", :mapped_mset, caller[0])
199
- mapped_mset(args[0])
200
- else
201
- call_command(args.unshift(:mset))
202
- end
203
- end
204
-
205
- def mapped_mset(hash)
206
- mset(*hash.to_a.flatten)
24
+ def connect
25
+ connect_to(@host, @port)
26
+ call(:auth, @password) if @password
27
+ call(:select, @db) if @db != 0
28
+ @sock
207
29
  end
208
30
 
209
- def msetnx(*args)
210
- if args.size == 1
211
- deprecated("msetnx with a hash", :mapped_msetnx, caller[0])
212
- mapped_msetnx(args[0])
213
- else
214
- call_command(args.unshift(:msetnx))
31
+ def call(name, *args)
32
+ ensure_connected do
33
+ process_and_read([[name, *args]]).first
215
34
  end
216
35
  end
217
36
 
218
- def mapped_msetnx(hash)
219
- msetnx(*hash.to_a.flatten)
220
- end
221
-
222
- # Similar to memcache.rb's #get_multi, returns a hash mapping
223
- # keys to values.
224
- def mapped_mget(*keys)
225
- result = {}
226
- mget(*keys).each do |value|
227
- key = keys.shift
228
- result.merge!(key => value) unless value.nil?
37
+ def call_async(name, *args)
38
+ ensure_connected do
39
+ process([[name, *args]])
229
40
  end
230
- result
231
- end
232
-
233
- def sort(key, options = {})
234
- cmd = []
235
- cmd << "SORT #{key}"
236
- cmd << "BY #{options[:by]}" if options[:by]
237
- cmd << "GET #{[options[:get]].flatten * ' GET '}" if options[:get]
238
- cmd << "#{options[:order]}" if options[:order]
239
- cmd << "LIMIT #{options[:limit].join(' ')}" if options[:limit]
240
- cmd << "STORE #{options[:store]}" if options[:store]
241
- call_command(cmd)
242
41
  end
243
42
 
244
- def incr(key, increment = nil)
245
- if increment
246
- deprecated("incr with an increment", :incrby, caller[0])
247
- incrby(key, increment)
248
- else
249
- call_command([:incr, key])
43
+ def call_pipelined(commands)
44
+ ensure_connected do
45
+ process_and_read(commands)
250
46
  end
251
47
  end
252
48
 
253
- def decr(key, decrement = nil)
254
- if decrement
255
- deprecated("decr with a decrement", :decrby, caller[0])
256
- decrby(key, decrement)
257
- else
258
- call_command([:decr, key])
49
+ def call_blocking(name, *args)
50
+ ensure_connected do
51
+ without_socket_timeout do
52
+ call(name, *args)
53
+ end
259
54
  end
260
55
  end
261
56
 
262
- # Ruby defines a now deprecated type method so we need to override it here
263
- # since it will never hit method_missing
264
- def type(key)
265
- call_command(['type', key])
266
- end
267
-
268
- def quit
269
- call_command(['quit'])
270
- rescue Errno::ECONNRESET
271
- end
272
-
273
- def pipelined(&block)
274
- Redis.deprecate("Calling pipelined commands on the yielded object will be deprecated in 2.0", caller[0])
275
- pipeline = Pipeline.new self
276
- yield pipeline
277
- pipeline.execute
278
- end
279
-
280
- def exec
281
- # Need to override Kernel#exec.
282
- call_command([:exec])
283
- end
284
-
285
- def multi(&block)
286
- result = call_command [:multi]
287
-
288
- return result unless block_given?
289
-
290
- begin
291
- yield(self)
292
- exec
293
- rescue Exception => e
294
- discard
295
- raise e
296
- end
57
+ def connected?
58
+ !! @sock
297
59
  end
298
60
 
299
- def subscribe(*classes)
300
- # Top-level `subscribe` MUST be called with a block,
301
- # nested `subscribe` MUST NOT be called with a block
302
- if !@pubsub && !block_given?
303
- raise "Top-level subscribe requires a block"
304
- elsif @pubsub == true && block_given?
305
- raise "Nested subscribe does not take a block"
306
- elsif @pubsub
307
- # If we're already pubsub'ing, just subscribe us to some more classes
308
- call_command [:subscribe,*classes]
309
- return true
310
- end
311
-
312
- @pubsub = true
313
- call_command [:subscribe,*classes]
314
- sub = Subscription.new
315
- yield(sub)
61
+ def disconnect
316
62
  begin
317
- while true
318
- type, *reply = read_reply # type, [class,data]
319
- case type
320
- when 'subscribe','unsubscribe'
321
- sub.send(type) && sub.send(type).call(reply[0],reply[1])
322
- when 'message'
323
- sub.send(type) && sub.send(type).call(reply[0],reply[1])
324
- end
325
- break if type == 'unsubscribe' && reply[1] == 0
326
- end
327
- rescue RuntimeError
328
- call_command [:unsubscribe]
329
- raise
63
+ @sock.close
64
+ rescue
330
65
  ensure
331
- @pubsub = false
332
- end
333
- end
334
-
335
- # Wrap raw_call_command to handle reconnection on socket error. We
336
- # try to reconnect just one time, otherwise let the error araise.
337
- def call_command(argv)
338
- log(argv.inspect, :debug)
339
-
340
- connect_to_server unless connected?
341
-
342
- begin
343
- raw_call_command(argv.dup)
344
- rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED
345
- if reconnect
346
- raw_call_command(argv.dup)
347
- else
348
- raise Errno::ECONNRESET
349
- end
350
- end
351
- end
352
-
353
- def server
354
- "#{@host}:#{@port}"
355
- end
356
-
357
- def connect_to(host, port)
358
-
359
- # We support connect_to() timeout only if system_timer is availabe
360
- # or if we are running against Ruby >= 1.9
361
- # Timeout reading from the socket instead will be supported anyway.
362
- if @timeout != 0 and Timer
363
- begin
364
- @sock = TCPSocket.new(host, port)
365
- rescue Timeout::Error
366
- @sock = nil
367
- raise Timeout::Error, "Timeout connecting to the server"
368
- end
369
- else
370
- @sock = TCPSocket.new(host, port)
371
- end
372
-
373
- @sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
374
-
375
- # If the timeout is set we set the low level socket options in order
376
- # to make sure a blocking read will return after the specified number
377
- # of seconds. This hack is from memcached ruby client.
378
- set_socket_timeout!(@timeout) if @timeout
379
-
380
- rescue Errno::ECONNREFUSED
381
- raise Errno::ECONNREFUSED, "Unable to connect to Redis on #{host}:#{port}"
382
- end
383
-
384
- def connect_to_server
385
- connect_to(@host, @port)
386
- call_command([:auth, @password]) if @password
387
- call_command([:select, @db]) if @db != 0
388
- @sock
389
- end
390
-
391
- def method_missing(*argv)
392
- call_command(argv)
393
- end
394
-
395
- def raw_call_command(argvp)
396
- if argvp[0].is_a?(Array)
397
- argvv = argvp
398
- pipeline = true
399
- else
400
- argvv = [argvp]
401
- pipeline = false
402
- end
403
-
404
- if @binary_keys or pipeline or MULTI_BULK_COMMANDS[argvv[0][0].to_s]
405
- command = ""
406
- argvv.each do |argv|
407
- command << "*#{argv.size}\r\n"
408
- argv.each{|a|
409
- a = a.to_s
410
- command << "$#{get_size(a)}\r\n"
411
- command << a
412
- command << "\r\n"
413
- }
414
- end
415
- else
416
- command = ""
417
- argvv.each do |argv|
418
- bulk = nil
419
- argv[0] = argv[0].to_s
420
- if ALIASES[argv[0]]
421
- deprecated(argv[0], ALIASES[argv[0]], caller[4])
422
- argv[0] = ALIASES[argv[0]]
423
- end
424
- raise "#{argv[0]} command is disabled" if DISABLED_COMMANDS[argv[0]]
425
- if BULK_COMMANDS[argv[0]] and argv.length > 1
426
- bulk = argv[-1].to_s
427
- argv[-1] = get_size(bulk)
428
- end
429
- command << "#{argv.join(' ')}\r\n"
430
- command << "#{bulk}\r\n" if bulk
431
- end
432
- end
433
- # When in Pub/Sub mode we don't read replies synchronously.
434
- if @pubsub
435
- @sock.write(command)
436
- return true
437
- end
438
- # The normal command execution is reading and processing the reply.
439
- results = maybe_lock do
440
- begin
441
- set_socket_timeout!(0) if requires_timeout_reset?(argvv[0][0].to_s)
442
- process_command(command, argvv)
443
- ensure
444
- set_socket_timeout!(@timeout) if requires_timeout_reset?(argvv[0][0].to_s)
445
- end
446
- end
447
-
448
- return pipeline ? results : results[0]
449
- end
450
-
451
- def process_command(command, argvv)
452
- @sock.write(command)
453
- argvv.map do |argv|
454
- processor = REPLY_PROCESSOR[argv[0].to_s]
455
- processor ? processor.call(read_reply) : read_reply
66
+ @sock = nil
456
67
  end
68
+ true
457
69
  end
458
70
 
459
- def maybe_lock(&block)
460
- if @thread_safe
461
- @mutex.synchronize(&block)
462
- else
463
- block.call
464
- end
71
+ def reconnect
72
+ disconnect && connect
465
73
  end
466
74
 
467
- def read_reply
75
+ def read
468
76
 
469
77
  # We read the first byte using read() mainly because gets() is
470
78
  # immune to raw socket timeouts.
@@ -485,21 +93,30 @@ class Redis
485
93
  format_reply(reply_type, @sock.gets)
486
94
  end
487
95
 
488
-
489
- if "".respond_to?(:bytesize)
490
- def get_size(string)
491
- string.bytesize
492
- end
493
- else
494
- def get_size(string)
495
- string.size
96
+ def without_socket_timeout
97
+ begin
98
+ self.timeout = 0
99
+ yield
100
+ ensure
101
+ self.timeout = @timeout
496
102
  end
497
103
  end
498
104
 
499
- private
105
+ protected
106
+
107
+ def build_command(name, *args)
108
+ command = []
109
+ command << "*#{args.size + 1}"
110
+ command << "$#{string_size name}"
111
+ command << name
500
112
 
501
- def log(str, level = :info)
502
- @logger.send(level, str.to_s) if @logger
113
+ args.each do |arg|
114
+ arg = arg.to_s
115
+ command << "$#{string_size arg}"
116
+ command << arg
117
+ end
118
+
119
+ command
503
120
  end
504
121
 
505
122
  def deprecated(old, new = nil, trace = caller[0])
@@ -508,39 +125,37 @@ class Redis
508
125
  Redis.deprecate(message, trace)
509
126
  end
510
127
 
511
- def requires_timeout_reset?(command)
512
- BLOCKING_COMMANDS[command] && @timeout
513
- end
128
+ COMMAND_DELIMITER = "\r\n"
514
129
 
515
- def set_socket_timeout!(timeout)
516
- secs = Integer(timeout)
517
- usecs = Integer((timeout - secs) * 1_000_000)
518
- optval = [secs, usecs].pack("l_2")
519
- begin
520
- @sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval
521
- @sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval
522
- rescue Exception => e
523
- # Solaris, for one, does not like/support socket timeouts.
524
- log("Unable to use raw socket timeouts: #{e.class.name}: #{e.message}")
130
+ def process(commands)
131
+ logging(commands) do
132
+ @sock.write(join_commands(commands))
133
+ yield if block_given?
525
134
  end
526
135
  end
527
136
 
528
- def connected?
529
- !! @sock
137
+ def join_commands(commands)
138
+ commands.map do |command|
139
+ build_command(*command).join(COMMAND_DELIMITER) + COMMAND_DELIMITER
140
+ end.join(COMMAND_DELIMITER) + COMMAND_DELIMITER
530
141
  end
531
142
 
532
- def disconnect
533
- begin
534
- @sock.close
535
- rescue
536
- ensure
537
- @sock = nil
143
+ def process_and_read(commands)
144
+ process(commands) do
145
+ @mutex.synchronize do
146
+ Array.new(commands.size).map { read }
147
+ end
538
148
  end
539
- true
540
149
  end
541
150
 
542
- def reconnect
543
- disconnect && connect_to_server
151
+ if "".respond_to?(:bytesize)
152
+ def string_size(string)
153
+ string.to_s.bytesize
154
+ end
155
+ else
156
+ def string_size(string)
157
+ string.to_s.size
158
+ end
544
159
  end
545
160
 
546
161
  def format_reply(reply_type, line)
@@ -576,8 +191,74 @@ class Redis
576
191
 
577
192
  def format_multi_bulk_reply(line)
578
193
  reply = []
579
- line.to_i.times { reply << read_reply }
194
+ line.to_i.times { reply << read }
580
195
  reply
581
196
  end
197
+
198
+ def logging(commands)
199
+ return yield unless @logger
200
+
201
+ begin
202
+ commands.each do |name, *args|
203
+ @logger.debug("Redis >> #{name.to_s.upcase} #{args.join(" ")}")
204
+ end
205
+
206
+ t1 = Time.now
207
+ yield
208
+ ensure
209
+ @logger.debug("Redis >> %0.2fms" % ((Time.now - t1) * 1000))
210
+ end
211
+ end
212
+
213
+ if defined?(Timeout)
214
+ TimeoutError = Timeout::Error
215
+ else
216
+ TimeoutError = Exception
217
+ end
218
+
219
+ def connect_to(host, port)
220
+ begin
221
+ @sock = TCPSocket.new(host, port)
222
+ rescue TimeoutError
223
+ @sock = nil
224
+ raise
225
+ end
226
+
227
+ @sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
228
+
229
+ # If the timeout is set we set the low level socket options in order
230
+ # to make sure a blocking read will return after the specified number
231
+ # of seconds. This hack is from memcached ruby client.
232
+ self.timeout = @timeout
233
+
234
+ rescue Errno::ECONNREFUSED
235
+ raise Errno::ECONNREFUSED, "Unable to connect to Redis on #{host}:#{port}"
236
+ end
237
+
238
+ def timeout=(timeout)
239
+ secs = Integer(timeout)
240
+ usecs = Integer((timeout - secs) * 1_000_000)
241
+ optval = [secs, usecs].pack("l_2")
242
+
243
+ begin
244
+ @sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval
245
+ @sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval
246
+ rescue Errno::ENOPROTOOPT
247
+ end
248
+ end
249
+
250
+ def ensure_connected
251
+ connect unless connected?
252
+
253
+ begin
254
+ yield
255
+ rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED
256
+ if reconnect
257
+ yield
258
+ else
259
+ raise Errno::ECONNRESET
260
+ end
261
+ end
262
+ end
582
263
  end
583
264
  end