redis 1.0.7 → 2.0.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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