em-redis-unified 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ .DS_Store
2
+ pkg
data/History.txt ADDED
@@ -0,0 +1,22 @@
1
+ == 0.2.2 / 2009-12-29
2
+ * reselect database after reconnecting
3
+
4
+ == 0.2.1 / 2009-12-15
5
+ * updated gem dependencies
6
+
7
+ == 0.2 / 2009-12-15
8
+ * rip off stock redis gem
9
+ * sort is no longer compatible with 0.1 version
10
+ * response of exists, sismember, sadd, srem, smove, zadd, zrem, move, setnx, del, renamenx, and expire is either true or false, not 0 or 1 as in 0.1
11
+ * info returns hash of symbols now
12
+ * lrem has different argument order
13
+
14
+ == 0.1.1 / 2009-05-01
15
+
16
+ * added a number of aliases to redis-based method names
17
+ * refactored process_cmd method for greater clarity
18
+
19
+ == 0.1.0 / 2009-04-28
20
+
21
+ * initial release
22
+ * compatible with Redis 0.093
data/README.rdoc ADDED
@@ -0,0 +1,86 @@
1
+ == EM-REDIS
2
+
3
+ == DESCRIPTION:
4
+
5
+ An EventMachine[http://rubyeventmachine.com/] based library for interacting with the very cool Redis[http://code.google.com/p/redis/] data store by Salvatore 'antirez' Sanfilippo.
6
+ Modeled after eventmachine's implementation of the memcached protocol, and influenced by Ezra Zygmuntowicz's {redis-rb}[http://github.com/ezmobius/redis-rb/tree/master] library (distributed as part of Redis).
7
+
8
+ This library is only useful when used as part of an application that relies on
9
+ Event Machine's event loop. It implements an EM-based client protocol, which
10
+ leverages the non-blocking nature of the EM interface to achieve significant
11
+ parallelization without threads.
12
+
13
+
14
+ == FEATURES/PROBLEMS:
15
+
16
+ Implements most Redis commands (see {the list of available commands here}[http://code.google.com/p/redis/wiki/CommandReference] with the notable
17
+ exception of MONITOR.
18
+
19
+ == SYNOPSIS:
20
+
21
+ Like any Deferrable eventmachine-based protocol implementation, using EM-Redis involves making calls and passing blocks that serve as callbacks when the call returns.
22
+
23
+ require 'em-redis'
24
+
25
+ EM.run do
26
+ redis = EM::Protocols::Redis.connect
27
+ redis.errback do |err|
28
+ puts err.inspect
29
+ puts "Error code: #{err.code}" if err.code
30
+ end
31
+ redis.set "a", "foo" do |response|
32
+ redis.get "a" do |response|
33
+ puts response
34
+ end
35
+ end
36
+ # We get pipelining for free
37
+ redis.set("b", "bar")
38
+ redis.get("a") do |response|
39
+ puts response # will be foo
40
+ end
41
+ end
42
+
43
+ To run tests on a Redis server (currently compatible with 1.3)
44
+
45
+ rake
46
+
47
+ Because the EM::Protocol::Memcached code used Bacon for testing, test code is
48
+ currently in the form of bacon specs.
49
+
50
+ == REQUIREMENTS:
51
+
52
+ * Redis (download[http://code.google.com/p/redis/downloads/list])
53
+
54
+ == INSTALL:
55
+
56
+ sudo gem install em-redis
57
+
58
+ == LICENSE:
59
+
60
+ (The MIT License)
61
+
62
+ Copyright (c) 2008, 2009
63
+
64
+ Permission is hereby granted, free of charge, to any person obtaining
65
+ a copy of this software and associated documentation files (the
66
+ 'Software'), to deal in the Software without restriction, including
67
+ without limitation the rights to use, copy, modify, merge, publish,
68
+ distribute, sublicense, and/or sell copies of the Software, and to
69
+ permit persons to whom the Software is furnished to do so, subject to
70
+ the following conditions:
71
+
72
+ The above copyright notice and this permission notice shall be
73
+ included in all copies or substantial portions of the Software.
74
+
75
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
76
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
77
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
78
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
79
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
80
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
81
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
82
+
83
+ == CREDIT
84
+
85
+ by Jonathan Broad (http://www.relativepath.org)
86
+
data/Rakefile ADDED
@@ -0,0 +1,41 @@
1
+ # Look in the tasks/setup.rb file for the various options that can be
2
+ # configured in this Rakefile. The .rake files in the tasks directory
3
+ # are where the options are used.
4
+
5
+ begin
6
+ require 'bones'
7
+ rescue LoadError
8
+ abort '### Please install the "bones" gem ###'
9
+ end
10
+
11
+ ensure_in_path 'lib'
12
+ require 'em-redis'
13
+
14
+ task :default => ['redis:test']
15
+
16
+ Bones {
17
+ name 'em-redis-unified'
18
+ authors ['Jonathan Broad', 'Eugene Pimenov', 'Sean Porter']
19
+ email 'portertech@gmail.com'
20
+ url 'http://github.com/portertech/em-redis'
21
+ summary 'An eventmachine-based implementation of the Redis protocol'
22
+ description summary
23
+ version EMRedis::VERSION
24
+
25
+ readme_file 'README.rdoc'
26
+ ignore_file '.gitignore'
27
+
28
+ depend_on 'eventmachine', '>=0.12.10'
29
+
30
+ depend_on "bacon", :development => true
31
+ depend_on "em-spec", :development => true
32
+ }
33
+
34
+ namespace :redis do
35
+ desc "Test em-redis against a live Redis"
36
+ task :test do
37
+ sh "bacon spec/live_redis_protocol_spec.rb spec/redis_commands_spec.rb spec/redis_protocol_spec.rb"
38
+ end
39
+ end
40
+
41
+ # EOF
data/lib/em-redis.rb ADDED
@@ -0,0 +1,47 @@
1
+
2
+ module EMRedis
3
+
4
+ # :stopdoc:
5
+ VERSION = '0.4.0'
6
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
7
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
8
+ # :startdoc:
9
+
10
+ # Returns the version string for the library.
11
+ #
12
+ def self.version
13
+ VERSION
14
+ end
15
+
16
+ # Returns the library path for the module. If any arguments are given,
17
+ # they will be joined to the end of the libray path using
18
+ # <tt>File.join</tt>.
19
+ #
20
+ def self.libpath( *args )
21
+ args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
22
+ end
23
+
24
+ # Returns the lpath for the module. If any arguments are given,
25
+ # they will be joined to the end of the path using
26
+ # <tt>File.join</tt>.
27
+ #
28
+ def self.path( *args )
29
+ args.empty? ? PATH : ::File.join(PATH, args.flatten)
30
+ end
31
+
32
+ # Utility method used to require all files ending in .rb that lie in the
33
+ # directory below this file that has the same name as the filename passed
34
+ # in. Optionally, a specific _directory_ name can be passed in such that
35
+ # the _filename_ does not have to be equivalent to the directory.
36
+ #
37
+ def self.require_all_libs_relative_to( fname, dir = nil )
38
+ dir ||= ::File.basename(fname, '.*')
39
+ search_me = ::File.expand_path(
40
+ ::File.join(::File.dirname(fname), dir, '**', '*.rb'))
41
+
42
+ Dir.glob(search_me).sort.each {|rb| require rb}
43
+ end
44
+
45
+ end # module EMRedis
46
+
47
+ EMRedis.require_all_libs_relative_to(__FILE__)
@@ -0,0 +1,511 @@
1
+ require 'rubygems'
2
+ require 'eventmachine'
3
+ require 'uri'
4
+
5
+ module EventMachine
6
+ module Protocols
7
+ module Redis
8
+ include EM::Deferrable
9
+
10
+ ##
11
+ # constants
12
+ #########################
13
+
14
+ OK = "OK".freeze
15
+ MINUS = "-".freeze
16
+ PLUS = "+".freeze
17
+ COLON = ":".freeze
18
+ DOLLAR = "$".freeze
19
+ ASTERISK = "*".freeze
20
+ DELIM = "\r\n".freeze
21
+
22
+ BOOLEAN_PROCESSOR = lambda{|r| %w(1 OK).include? r.to_s}
23
+
24
+ REPLY_PROCESSOR = {
25
+ "exists" => BOOLEAN_PROCESSOR,
26
+ "sismember" => BOOLEAN_PROCESSOR,
27
+ "sadd" => BOOLEAN_PROCESSOR,
28
+ "srem" => BOOLEAN_PROCESSOR,
29
+ "smove" => BOOLEAN_PROCESSOR,
30
+ "zadd" => BOOLEAN_PROCESSOR,
31
+ "zrem" => BOOLEAN_PROCESSOR,
32
+ "move" => BOOLEAN_PROCESSOR,
33
+ "setnx" => BOOLEAN_PROCESSOR,
34
+ "del" => BOOLEAN_PROCESSOR,
35
+ "renamenx" => BOOLEAN_PROCESSOR,
36
+ "expire" => BOOLEAN_PROCESSOR,
37
+ "select" => BOOLEAN_PROCESSOR,
38
+ "hexists" => BOOLEAN_PROCESSOR,
39
+ "hset" => BOOLEAN_PROCESSOR,
40
+ "hdel" => BOOLEAN_PROCESSOR,
41
+ "hsetnx" => BOOLEAN_PROCESSOR,
42
+ "hgetall" => lambda{|r| Hash[*r]},
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
+ "push_tail" => "rpush",
62
+ "push_head" => "lpush",
63
+ "pop_tail" => "rpop",
64
+ "pop_head" => "lpop",
65
+ "list_set" => "lset",
66
+ "list_range" => "lrange",
67
+ "list_trim" => "ltrim",
68
+ "list_index" => "lindex",
69
+ "list_rm" => "lrem",
70
+ "set_add" => "sadd",
71
+ "set_delete" => "srem",
72
+ "set_count" => "scard",
73
+ "set_member?" => "sismember",
74
+ "set_members" => "smembers",
75
+ "set_intersect" => "sinter",
76
+ "set_intersect_store" => "sinterstore",
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
+ "rename_unless_exists" => "renamenx",
85
+ "type?" => "type",
86
+ "zset_add" => "zadd",
87
+ "zset_count" => "zcard",
88
+ "zset_range_by_score" => "zrangebyscore",
89
+ "zset_reverse_range" => "zrevrange",
90
+ "zset_range" => "zrange",
91
+ "zset_delete" => "zrem",
92
+ "zset_score" => "zscore",
93
+ "zset_incr_by" => "zincrby",
94
+ "zset_increment_by" => "zincrby",
95
+ "background_save" => 'bgsave',
96
+ "async_save" => 'bgsave',
97
+ "members" => 'smembers',
98
+ "decrement_by" => "decrby",
99
+ "decrement" => "decr",
100
+ "increment_by" => "incrby",
101
+ "increment" => "incr",
102
+ "set_if_nil" => "setnx",
103
+ "multi_get" => "mget",
104
+ "random_key" => "randomkey",
105
+ "random" => "randomkey",
106
+ "rename_if_nil" => "renamenx",
107
+ "tail_pop" => "rpop",
108
+ "pop" => "rpop",
109
+ "head_pop" => "lpop",
110
+ "shift" => "lpop",
111
+ "list_remove" => "lrem",
112
+ "index" => "lindex",
113
+ "trim" => "ltrim",
114
+ "list_range" => "lrange",
115
+ "range" => "lrange",
116
+ "list_len" => "llen",
117
+ "len" => "llen",
118
+ "head_push" => "lpush",
119
+ "unshift" => "lpush",
120
+ "tail_push" => "rpush",
121
+ "push" => "rpush",
122
+ "add" => "sadd",
123
+ "set_remove" => "srem",
124
+ "set_size" => "scard",
125
+ "member?" => "sismember",
126
+ "intersect" => "sinter",
127
+ "intersect_and_store" => "sinterstore",
128
+ "members" => "smembers",
129
+ "exists?" => "exists"
130
+ }
131
+
132
+ DISABLED_COMMANDS = {
133
+ "monitor" => true,
134
+ "sync" => true
135
+ }
136
+
137
+ def []=(key,value)
138
+ set(key,value)
139
+ end
140
+
141
+ def set(key, value, expiry=nil)
142
+ call_command(["set", key, value]) do |s|
143
+ yield s if block_given?
144
+ end
145
+ expire(key, expiry) if expiry
146
+ end
147
+
148
+ def sort(key, options={}, &blk)
149
+ cmd = ["sort", key]
150
+ cmd << ["by", options[:by]] if options[:by]
151
+ Array(options[:get]).each do |v|
152
+ cmd << ["get", v]
153
+ end
154
+ cmd << options[:order].split(" ") if options[:order]
155
+ cmd << ["limit", options[:limit]] if options[:limit]
156
+ cmd << ["store", options[:store]] if options[:store]
157
+ call_command(cmd.flatten, &blk)
158
+ end
159
+
160
+ def incr(key, increment = nil, &blk)
161
+ call_command(increment ? ["incrby",key,increment] : ["incr",key], &blk)
162
+ end
163
+
164
+ def decr(key, decrement = nil, &blk)
165
+ call_command(decrement ? ["decrby",key,decrement] : ["decr",key], &blk)
166
+ end
167
+
168
+ def select(db, &blk)
169
+ @db = db.to_i
170
+ call_command(['select', @db], &blk)
171
+ end
172
+
173
+ def auth(password, &blk)
174
+ @password = password
175
+ call_command(['auth', password], &blk)
176
+ end
177
+
178
+ # Similar to memcache.rb's #get_multi, returns a hash mapping
179
+ # keys to values.
180
+ def mapped_mget(*keys)
181
+ mget(*keys) do |response|
182
+ result = {}
183
+ response.each do |value|
184
+ key = keys.shift
185
+ result.merge!(key => value) unless value.nil?
186
+ end
187
+ yield result if block_given?
188
+ end
189
+ end
190
+
191
+ # Ruby defines a now deprecated type method so we need to override it here
192
+ # since it will never hit method_missing
193
+ def type(key, &blk)
194
+ call_command(['type', key], &blk)
195
+ end
196
+
197
+ def quit(&blk)
198
+ call_command(['quit'], &blk)
199
+ end
200
+
201
+ def errback(&blk)
202
+ @error_callback = blk
203
+ end
204
+ alias_method :on_error, :errback
205
+
206
+ def error(klass, msg)
207
+ err = klass.new(msg)
208
+ err.code = msg if err.respond_to?(:code)
209
+ @error_callback.call(err)
210
+ end
211
+
212
+ def before_reconnect(&blk)
213
+ @reconnect_callbacks[:before] = blk
214
+ end
215
+
216
+ def after_reconnect(&blk)
217
+ @reconnect_callbacks[:after] = blk
218
+ end
219
+
220
+ def method_missing(*argv, &blk)
221
+ call_command(argv, &blk)
222
+ end
223
+
224
+ def call_command(argv, &blk)
225
+ callback { raw_call_command(argv, &blk) }
226
+ end
227
+
228
+ def raw_call_command(argv, &blk)
229
+ argv[0] = argv[0].to_s unless argv[0].kind_of? String
230
+ argv[0] = argv[0].downcase
231
+ send_command(argv)
232
+ @redis_callbacks << [REPLY_PROCESSOR[argv[0]], blk]
233
+ end
234
+
235
+ def call_commands(argvs, &blk)
236
+ callback { raw_call_commands(argvs, &blk) }
237
+ end
238
+
239
+ def raw_call_commands(argvs, &blk)
240
+ if argvs.empty? # Shortcut
241
+ blk.call []
242
+ return
243
+ end
244
+
245
+ argvs.each do |argv|
246
+ argv[0] = argv[0].to_s unless argv[0].kind_of? String
247
+ send_command argv
248
+ end
249
+ # FIXME: argvs may contain heterogenous commands, storing all
250
+ # REPLY_PROCESSORs may turn out expensive and has been omitted
251
+ # for now.
252
+ @redis_callbacks << [nil, argvs.length, blk]
253
+ end
254
+
255
+ def send_command(argv)
256
+ argv = argv.dup
257
+
258
+ error DisabledCommand, "#{argv[0]} command is disabled" if DISABLED_COMMANDS[argv[0]]
259
+ argv[0] = ALIASES[argv[0]] if ALIASES[argv[0]]
260
+
261
+ if argv[-1].is_a?(Hash)
262
+ argv[-1] = argv[-1].to_a
263
+ argv.flatten!
264
+ end
265
+
266
+ command = ["*#{argv.size}"]
267
+ argv.each do |v|
268
+ v = v.to_s
269
+ command << "$#{get_size(v)}"
270
+ command << v
271
+ end
272
+ command = command.map {|cmd| cmd + DELIM}.join
273
+
274
+ @logger.debug { "*** sending: #{command}" } if @logger
275
+ send_data command
276
+ end
277
+
278
+ ##
279
+ # errors
280
+ #########################
281
+
282
+ class DisabledCommand < StandardError; end
283
+ class ParserError < StandardError; end
284
+ class ProtocolError < StandardError; end
285
+ class ConnectionError < StandardError; end
286
+
287
+ class RedisError < StandardError
288
+ attr_accessor :code
289
+
290
+ def initialize(*args)
291
+ args[0] = "Redis server returned error code: #{args[0]}"
292
+ super
293
+ end
294
+ end
295
+
296
+ ##
297
+ # em hooks
298
+ #########################
299
+
300
+ class << self
301
+ def parse_url(url)
302
+ begin
303
+ uri = URI.parse(url)
304
+ {
305
+ :host => uri.host,
306
+ :port => uri.port,
307
+ :password => uri.password
308
+ }
309
+ rescue
310
+ error ArgumentError, 'invalid redis url'
311
+ end
312
+ end
313
+
314
+ def connect(*args)
315
+ case args.length
316
+ when 0
317
+ options = {}
318
+ when 1
319
+ arg = args.shift
320
+ case arg
321
+ when Hash then options = arg
322
+ when String then options = parse_url(arg)
323
+ else error ArgumentError, 'first argument must be Hash or String'
324
+ end
325
+ when 2
326
+ options = {:host => args[1], :port => args[2]}
327
+ else
328
+ error ArgumentError, "wrong number of arguments (#{args.length} for 1)"
329
+ end
330
+ options[:host] ||= '127.0.0.1'
331
+ options[:port] = (options[:port] || 6379).to_i
332
+ EM.connect options[:host], options[:port], self, options
333
+ end
334
+ end
335
+
336
+ def initialize(options = {})
337
+ @host = options[:host]
338
+ @port = options[:port]
339
+ @db = (options[:db] || 0).to_i
340
+ @password = options[:password]
341
+ @auto_reconnect = options[:auto_reconnect] || true
342
+ @logger = options[:logger]
343
+ @error_callback = lambda do |err|
344
+ raise err
345
+ end
346
+ @reconnect_callbacks = {
347
+ :before => lambda{},
348
+ :after => lambda{}
349
+ }
350
+ @values = []
351
+
352
+ # These commands should be first
353
+ auth_and_select_db
354
+ end
355
+
356
+ def auth_and_select_db
357
+ call_command(["auth", @password]) if @password
358
+ call_command(["select", @db]) unless @db == 0
359
+ end
360
+ private :auth_and_select_db
361
+
362
+ def connection_completed
363
+ @logger.debug { "Connected to #{@host}:#{@port}" } if @logger
364
+
365
+ @reconnect_callbacks[:after].call if @reconnecting
366
+
367
+ @redis_callbacks = []
368
+ @multibulk_n = false
369
+ @reconnecting = false
370
+ @connected = true
371
+
372
+ succeed
373
+ end
374
+
375
+ # 19Feb09 Switched to a custom parser, LineText2 is recursive and can cause
376
+ # stack overflows when there is too much data.
377
+ # include EM::P::LineText2
378
+ def receive_data(data)
379
+ (@buffer ||= '') << data
380
+ while index = @buffer.index(DELIM)
381
+ begin
382
+ line = @buffer.slice!(0, index+2)
383
+ process_cmd line
384
+ rescue ParserError
385
+ @buffer[0...0] = line
386
+ break
387
+ end
388
+ end
389
+ end
390
+
391
+ def process_cmd(line)
392
+ @logger.debug { "*** processing #{line}" } if @logger
393
+ # first character of buffer will always be the response type
394
+ reply_type = line[0, 1]
395
+ reply_args = line.slice(1..-3) # remove type character and \r\n
396
+ case reply_type
397
+ # e.g. -ERR
398
+ when MINUS
399
+ # server ERROR
400
+ dispatch_error(reply_args)
401
+ # e.g. +OK
402
+ when PLUS
403
+ dispatch_response(reply_args)
404
+ # e.g. $3\r\nabc\r\n
405
+ # 'bulk' is more complex because it could be part of multi-bulk
406
+ when DOLLAR
407
+ data_len = Integer(reply_args)
408
+ if data_len == -1 # expect no data; return nil
409
+ dispatch_response(nil)
410
+ elsif @buffer.size >= data_len + 2 # buffer is full of expected data
411
+ dispatch_response(@buffer.slice!(0, data_len))
412
+ @buffer.slice!(0,2) # tossing \r\n
413
+ else # buffer isn't full or nil
414
+ raise ParserError
415
+ end
416
+ # e.g. :8
417
+ when COLON
418
+ dispatch_response(Integer(reply_args))
419
+ # e.g. *2\r\n$1\r\na\r\n$1\r\nb\r\n
420
+ when ASTERISK
421
+ multibulk_count = Integer(reply_args)
422
+ if multibulk_count == -1 || multibulk_count == 0
423
+ dispatch_response([])
424
+ else
425
+ start_multibulk(multibulk_count)
426
+ end
427
+ # WAT?
428
+ else
429
+ error ProtocolError, "reply type not recognized: #{line.strip}"
430
+ end
431
+ end
432
+
433
+ def dispatch_error(code)
434
+ @redis_callbacks.shift
435
+ error RedisError, code
436
+ end
437
+
438
+ def dispatch_response(value)
439
+ if @multibulk_n
440
+ @multibulk_values << value
441
+ @multibulk_n -= 1
442
+
443
+ if @multibulk_n == 0
444
+ value = @multibulk_values
445
+ @multibulk_n = false
446
+ else
447
+ return
448
+ end
449
+ end
450
+
451
+ callback = @redis_callbacks.shift
452
+ if callback.kind_of?(Array) && callback.length == 2
453
+ processor, blk = callback
454
+ value = processor.call(value) if processor
455
+ blk.call(value) if blk
456
+ elsif callback.kind_of?(Array) && callback.length == 3
457
+ processor, pipeline_count, blk = callback
458
+ value = processor.call(value) if processor
459
+ @values << value
460
+ if pipeline_count > 1
461
+ @redis_callbacks.unshift [processor, pipeline_count - 1, blk]
462
+ else
463
+ blk.call(@values) if blk
464
+ @values = []
465
+ end
466
+ end
467
+ end
468
+
469
+ def start_multibulk(multibulk_count)
470
+ @multibulk_n = multibulk_count
471
+ @multibulk_values = []
472
+ end
473
+
474
+ def connected?
475
+ @connected || false
476
+ end
477
+
478
+ def close
479
+ @closing = true
480
+ close_after_writing
481
+ end
482
+
483
+ def unbind
484
+ @logger.debug { "Disconnected" } if @logger
485
+ if @closing
486
+ @reconnecting = false
487
+ elsif (@connected || @reconnecting) && @auto_reconnect
488
+ @reconnect_callbacks[:before].call if @connected
489
+ @reconnecting = true
490
+ EM.add_timer(1) do
491
+ @logger.debug { "Reconnecting to #{@host}:#{@port}" } if @logger
492
+ reconnect @host, @port
493
+ auth_and_select_db
494
+ end
495
+ elsif @connected
496
+ error ConnectionError, 'connection closed'
497
+ else
498
+ error ConnectionError, 'unable to connect to redis server'
499
+ end
500
+ @connected = false
501
+ @deferred_status = nil
502
+ end
503
+
504
+ private
505
+ def get_size(string)
506
+ string.respond_to?(:bytesize) ? string.bytesize : string.size
507
+ end
508
+
509
+ end
510
+ end
511
+ end