dsander-redis 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Ezra Zygmuntowicz
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,112 @@
1
+ # redis-rb
2
+
3
+ A Ruby client library for the [Redis](http://code.google.com/p/redis) key-value store.
4
+
5
+ ## Information about Redis
6
+
7
+ Redis is a key-value store with some interesting features:
8
+
9
+ 1. It's fast.
10
+ 2. Keys are strings but values are typed. Currently Redis supports strings, lists, sets, sorted sets and hashes. [Atomic operations](http://code.google.com/p/redis/wiki/CommandReference) can be done on all of these types.
11
+
12
+ See [the Redis homepage](http://code.google.com/p/redis/wiki/README) for more information.
13
+
14
+ ## Getting started
15
+
16
+ You can connect to Redis by instantiating the `Redis` class:
17
+
18
+ require "redis"
19
+
20
+ redis = Redis.new
21
+
22
+ This assumes Redis was started with default values listening on `localhost`, port 6379. If you need to connect to a remote server or a different port, try:
23
+
24
+ redis = Redis.new(:host => "10.0.1.1", :port => 6380)
25
+
26
+ Once connected, you can start running commands against Redis:
27
+
28
+ >> redis.set "foo", "bar"
29
+ => "OK"
30
+
31
+ >> redis.get "foo"
32
+ => "bar"
33
+
34
+ >> redis.sadd "users", "albert"
35
+ => true
36
+
37
+ >> redis.sadd "users", "bernard"
38
+ => true
39
+
40
+ >> redis.sadd "users", "charles"
41
+ => true
42
+
43
+ How many users?
44
+
45
+ >> redis.scard "users"
46
+ => 3
47
+
48
+ Is `albert` a user?
49
+
50
+ >> redis.sismember "users", "albert"
51
+ => true
52
+
53
+ Is `isabel` a user?
54
+
55
+ >> redis.sismember "users", "isabel"
56
+ => false
57
+
58
+ Handle groups:
59
+
60
+ >> redis.sadd "admins", "albert"
61
+ => true
62
+
63
+ >> redis.sadd "admins", "isabel"
64
+ => true
65
+
66
+ Users who are also admins:
67
+
68
+ >> redis.sinter "users", "admins"
69
+ => ["albert"]
70
+
71
+ Users who are not admins:
72
+
73
+ >> redis.sdiff "users", "admins"
74
+ => ["bernard", "charles"]
75
+
76
+ Admins who are not users:
77
+
78
+ >> redis.sdiff "admins", "users"
79
+ => ["isabel"]
80
+
81
+ All users and admins:
82
+
83
+ >> redis.sunion "admins", "users"
84
+ => ["albert", "bernard", "charles", "isabel"]
85
+
86
+
87
+ ## Storing objects
88
+
89
+ Redis only stores strings as values. If you want to store an object inside a key, you can use a serialization/deseralization mechanism like JSON:
90
+
91
+ >> redis.set "foo", [1, 2, 3].to_json
92
+ => OK
93
+
94
+ >> JSON.parse(redis.get("foo"))
95
+ => [1, 2, 3]
96
+
97
+ ## Executing multiple commands atomically
98
+
99
+ You can use `MULTI/EXEC` to run arbitrary commands in an atomic fashion:
100
+
101
+ redis.multi do
102
+ redis.set "foo", "bar"
103
+ redis.incr "baz"
104
+ end
105
+
106
+ ## More info
107
+
108
+ Check the [Redis Command Reference](http://code.google.com/p/redis/wiki/CommandReference) or check the tests to find out how to use this client.
109
+
110
+ ## Contributing
111
+
112
+ [Fork the project](http://github.com/ezmobius/redis-rb) and send pull requests. You can also ask for help at `#redis` on Freenode.
data/Rakefile ADDED
@@ -0,0 +1,75 @@
1
+ require 'rubygems'
2
+ require 'rake/gempackagetask'
3
+ require 'rake/testtask'
4
+ require 'tasks/redis.tasks'
5
+
6
+ $:.unshift File.join(File.dirname(__FILE__), 'lib')
7
+ require 'redis'
8
+
9
+ GEM = 'dsander-redis'
10
+ GEM_NAME = 'dsander-redis'
11
+ GEM_VERSION = Redis::VERSION
12
+ AUTHORS = ['Ezra Zygmuntowicz', 'Taylor Weibley', 'Matthew Clark', 'Brian McKinney', 'Salvatore Sanfilippo', 'Luca Guidi', 'Dominik Sander']
13
+ EMAIL = "ez@engineyard.com"
14
+ HOMEPAGE = "http://github.com/dsander/redis-rb"
15
+ SUMMARY = "Ruby client library for Redis, the key value storage server"
16
+
17
+ spec = Gem::Specification.new do |s|
18
+ s.name = GEM
19
+ s.version = GEM_VERSION
20
+ s.platform = Gem::Platform::RUBY
21
+ s.has_rdoc = true
22
+ s.extra_rdoc_files = ["LICENSE"]
23
+ s.summary = SUMMARY
24
+ s.description = s.summary
25
+ s.authors = AUTHORS
26
+ s.email = EMAIL
27
+ s.homepage = HOMEPAGE
28
+ s.require_path = 'lib'
29
+ s.autorequire = GEM
30
+ s.files = %w(LICENSE README.markdown Rakefile) + Dir.glob("{lib,tasks,spec}/**/*")
31
+ end
32
+
33
+ REDIS_DIR = File.expand_path(File.join("..", "test"), __FILE__)
34
+ REDIS_CNF = File.join(REDIS_DIR, "test.conf")
35
+ REDIS_PID = File.join(REDIS_DIR, "db", "redis.pid")
36
+
37
+ task :default => :run
38
+
39
+ desc "Run tests and manage server start/stop"
40
+ task :run => [:start, :test, :stop]
41
+
42
+ desc "Start the Redis server"
43
+ task :start do
44
+ unless File.exists?(REDIS_PID)
45
+ system "redis-server #{REDIS_CNF}"
46
+ end
47
+ end
48
+
49
+ desc "Stop the Redis server"
50
+ task :stop do
51
+ if File.exists?(REDIS_PID)
52
+ system "kill #{File.read(REDIS_PID)}"
53
+ system "rm #{REDIS_PID}"
54
+ end
55
+ end
56
+
57
+ Rake::TestTask.new(:test) do |t|
58
+ t.pattern = 'test/**/*_test.rb'
59
+ end
60
+
61
+ Rake::GemPackageTask.new(spec) do |pkg|
62
+ pkg.gem_spec = spec
63
+ end
64
+
65
+ desc "install the gem locally"
66
+ task :install => [:package] do
67
+ sh %{sudo gem install pkg/#{GEM}-#{GEM_VERSION}}
68
+ end
69
+
70
+ desc "create a gemspec file"
71
+ task :gemspec do
72
+ File.open("#{GEM}.gemspec", "w") do |file|
73
+ file.puts spec.to_ruby
74
+ end
75
+ end
data/lib/edis.rb ADDED
@@ -0,0 +1,3 @@
1
+ # This file allows for the running of redis-rb with a nice
2
+ # command line look-and-feel: irb -rubygems -redis foo.rb
3
+ require 'redis'
data/lib/redis.rb ADDED
@@ -0,0 +1,25 @@
1
+ require 'socket'
2
+
3
+ class Redis
4
+ VERSION = "1.0.6"
5
+
6
+ def self.new(*attrs)
7
+ Client.new(*attrs)
8
+ end
9
+ end
10
+
11
+ begin
12
+ if RUBY_VERSION >= '1.9'
13
+ require 'timeout'
14
+ Redis::Timer = Timeout
15
+ else
16
+ require 'system_timer'
17
+ Redis::Timer = SystemTimer
18
+ end
19
+ rescue LoadError
20
+ Redis::Timer = nil
21
+ end
22
+
23
+ require 'redis/client'
24
+ require 'redis/pipeline'
25
+ require 'redis/subscribe'
@@ -0,0 +1,577 @@
1
+ class Redis
2
+ 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
+ MINUS = "-".freeze
11
+ PLUS = "+".freeze
12
+ COLON = ":".freeze
13
+ DOLLAR = "$".freeze
14
+ ASTERISK = "*".freeze
15
+
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 = {}
67
+ r.each_line {|kv|
68
+ k,v = kv.split(":",2).map{|x| x.chomp}
69
+ info[k.to_sym] = v
70
+ }
71
+ info
72
+ },
73
+ "keys" => lambda{|r|
74
+ if r.is_a?(Array)
75
+ r
76
+ else
77
+ r.split(" ")
78
+ end
79
+ },
80
+ "hgetall" => lambda{|r|
81
+ Hash[*r]
82
+ }
83
+ }
84
+
85
+ ALIASES = {
86
+ "flush_db" => "flushdb",
87
+ "flush_all" => "flushall",
88
+ "last_save" => "lastsave",
89
+ "key?" => "exists",
90
+ "delete" => "del",
91
+ "randkey" => "randomkey",
92
+ "list_length" => "llen",
93
+ "push_tail" => "rpush",
94
+ "push_head" => "lpush",
95
+ "pop_tail" => "rpop",
96
+ "pop_head" => "lpop",
97
+ "list_set" => "lset",
98
+ "list_range" => "lrange",
99
+ "list_trim" => "ltrim",
100
+ "list_index" => "lindex",
101
+ "list_rm" => "lrem",
102
+ "set_add" => "sadd",
103
+ "set_delete" => "srem",
104
+ "set_count" => "scard",
105
+ "set_member?" => "sismember",
106
+ "set_members" => "smembers",
107
+ "set_intersect" => "sinter",
108
+ "set_intersect_store" => "sinterstore",
109
+ "set_inter_store" => "sinterstore",
110
+ "set_union" => "sunion",
111
+ "set_union_store" => "sunionstore",
112
+ "set_diff" => "sdiff",
113
+ "set_diff_store" => "sdiffstore",
114
+ "set_move" => "smove",
115
+ "set_unless_exists" => "setnx",
116
+ "rename_unless_exists" => "renamenx",
117
+ "type?" => "type",
118
+ "zset_add" => "zadd",
119
+ "zset_count" => "zcard",
120
+ "zset_range_by_score" => "zrangebyscore",
121
+ "zset_reverse_range" => "zrevrange",
122
+ "zset_range" => "zrange",
123
+ "zset_delete" => "zrem",
124
+ "zset_score" => "zscore",
125
+ "zset_incr_by" => "zincrby",
126
+ "zset_increment_by" => "zincrby"
127
+ }
128
+
129
+ DISABLED_COMMANDS = {
130
+ "monitor" => true,
131
+ "sync" => true
132
+ }
133
+
134
+ BLOCKING_COMMANDS = {
135
+ "blpop" => true,
136
+ "brpop" => true
137
+ }
138
+
139
+ def initialize(options = {})
140
+ @host = options[:host] || '127.0.0.1'
141
+ @port = (options[:port] || 6379).to_i
142
+ @db = (options[:db] || 0).to_i
143
+ @timeout = (options[:timeout] || 5).to_i
144
+ @password = options[:password]
145
+ @logger = options[:logger]
146
+ @thread_safe = options[:thread_safe]
147
+ @binary_keys = options[:binary_keys]
148
+ @evented = defined?(EM) && EM.reactor_running?
149
+ @mutex = ::Mutex.new if @thread_safe && !@evented
150
+ @sock = nil
151
+ @pubsub = false
152
+ @sock = nil
153
+ self.extend(Redis::EventedClient) if defined?(EM) and EM.reactor_running?
154
+ log(self)
155
+ end
156
+
157
+ def to_s
158
+ "Redis Client connected to #{server} against DB #{@db}"
159
+ end
160
+
161
+ def select(*args)
162
+ raise "SELECT not allowed, use the :db option when creating the object"
163
+ end
164
+
165
+ def [](key)
166
+ get(key)
167
+ end
168
+
169
+ def []=(key,value)
170
+ set(key, value)
171
+ end
172
+
173
+ def get(key)
174
+ call_command([:get, key])
175
+ end
176
+
177
+ def set(key, value, ttl = nil)
178
+ if ttl
179
+ deprecated("set with an expire", :set_with_expire, caller[0])
180
+ set_with_expire(key, value, ttl)
181
+ else
182
+ call_command([:set, key, value])
183
+ end
184
+ end
185
+
186
+ def set_with_expire(key, value, ttl)
187
+ multi do
188
+ set(key, value)
189
+ expire(key, ttl)
190
+ end
191
+ end
192
+
193
+ def mset(*args)
194
+ if args.size == 1
195
+ deprecated("mset with a hash", :mapped_mset, caller[0])
196
+ mapped_mset(args[0])
197
+ else
198
+ call_command(args.unshift(:mset))
199
+ end
200
+ end
201
+
202
+ def mapped_mset(hash)
203
+ mset(*hash.to_a.flatten)
204
+ end
205
+
206
+ def msetnx(*args)
207
+ if args.size == 1
208
+ deprecated("msetnx with a hash", :mapped_msetnx, caller[0])
209
+ mapped_msetnx(args[0])
210
+ else
211
+ call_command(args.unshift(:msetnx))
212
+ end
213
+ end
214
+
215
+ def mapped_msetnx(hash)
216
+ msetnx(*hash.to_a.flatten)
217
+ end
218
+
219
+ # Similar to memcache.rb's #get_multi, returns a hash mapping
220
+ # keys to values.
221
+ def mapped_mget(*keys)
222
+ result = {}
223
+ mget(*keys).each do |value|
224
+ key = keys.shift
225
+ result.merge!(key => value) unless value.nil?
226
+ end
227
+ result
228
+ end
229
+
230
+ def sort(key, options = {})
231
+ cmd = []
232
+ cmd << "SORT #{key}"
233
+ cmd << "BY #{options[:by]}" if options[:by]
234
+ cmd << "GET #{[options[:get]].flatten * ' GET '}" if options[:get]
235
+ cmd << "#{options[:order]}" if options[:order]
236
+ cmd << "LIMIT #{options[:limit].join(' ')}" if options[:limit]
237
+ cmd << "STORE #{options[:store]}" if options[:store]
238
+ call_command(cmd)
239
+ end
240
+
241
+ def incr(key, increment = nil)
242
+ if increment
243
+ deprecated("incr with an increment", :incrby, caller[0])
244
+ incrby(key, increment)
245
+ else
246
+ call_command([:incr, key])
247
+ end
248
+ end
249
+
250
+ def decr(key, decrement = nil)
251
+ if decrement
252
+ deprecated("decr with a decrement", :decrby, caller[0])
253
+ decrby(key, decrement)
254
+ else
255
+ call_command([:decr, key])
256
+ end
257
+ end
258
+
259
+ # Ruby defines a now deprecated type method so we need to override it here
260
+ # since it will never hit method_missing
261
+ def type(key)
262
+ call_command(['type', key])
263
+ end
264
+
265
+ def quit
266
+ call_command(['quit'])
267
+ rescue Errno::ECONNRESET
268
+ end
269
+
270
+ def pipelined(&block)
271
+ pipeline = Pipeline.new self
272
+ yield pipeline
273
+ pipeline.execute
274
+ end
275
+
276
+ def exec
277
+ # Need to override Kernel#exec.
278
+ call_command([:exec])
279
+ end
280
+
281
+ def multi(&block)
282
+ result = call_command [:multi]
283
+
284
+ return result unless block_given?
285
+
286
+ begin
287
+ yield(self)
288
+ exec
289
+ rescue Exception => e
290
+ discard
291
+ raise e
292
+ end
293
+ end
294
+
295
+ def subscribe(*classes)
296
+ # Top-level `subscribe` MUST be called with a block,
297
+ # nested `subscribe` MUST NOT be called with a block
298
+ if !@pubsub && !block_given?
299
+ raise "Top-level subscribe requires a block"
300
+ elsif @pubsub == true && block_given?
301
+ raise "Nested subscribe does not take a block"
302
+ elsif @pubsub
303
+ # If we're already pubsub'ing, just subscribe us to some more classes
304
+ call_command [:subscribe,*classes]
305
+ return true
306
+ end
307
+
308
+ @pubsub = true
309
+ call_command [:subscribe,*classes]
310
+ sub = Subscription.new
311
+ yield(sub)
312
+ begin
313
+ while true
314
+ type, *reply = read_reply # type, [class,data]
315
+ case type
316
+ when 'subscribe','unsubscribe'
317
+ sub.send(type) && sub.send(type).call(reply[0],reply[1])
318
+ when 'message'
319
+ sub.send(type) && sub.send(type).call(reply[0],reply[1])
320
+ end
321
+ break if type == 'unsubscribe' && reply[1] == 0
322
+ end
323
+ rescue RuntimeError
324
+ call_command [:unsubscribe]
325
+ raise
326
+ ensure
327
+ @pubsub = false
328
+ end
329
+ end
330
+
331
+ # Wrap raw_call_command to handle reconnection on socket error. We
332
+ # try to reconnect just one time, otherwise let the error araise.
333
+ def call_command(argv)
334
+ log(argv.inspect, :debug)
335
+
336
+ connect_to_server unless connected?
337
+
338
+ begin
339
+ raw_call_command(argv.dup)
340
+ rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED
341
+ if reconnect
342
+ raw_call_command(argv.dup)
343
+ else
344
+ raise Errno::ECONNRESET
345
+ end
346
+ end
347
+ end
348
+
349
+ def server
350
+ "#{@host}:#{@port}"
351
+ end
352
+
353
+ def connect_to(host, port)
354
+
355
+ # We support connect_to() timeout only if system_timer is availabe
356
+ # or if we are running against Ruby >= 1.9
357
+ # Timeout reading from the socket instead will be supported anyway.
358
+ if @timeout != 0 and Timer
359
+ begin
360
+ @sock = TCPSocket.new(host, port)
361
+ rescue Timeout::Error
362
+ @sock = nil
363
+ raise Timeout::Error, "Timeout connecting to the server"
364
+ end
365
+ else
366
+ @sock = TCPSocket.new(host, port)
367
+ end
368
+
369
+ @sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
370
+
371
+ # If the timeout is set we set the low level socket options in order
372
+ # to make sure a blocking read will return after the specified number
373
+ # of seconds. This hack is from memcached ruby client.
374
+ set_socket_timeout!(@timeout) if @timeout
375
+
376
+ rescue Errno::ECONNREFUSED
377
+ raise Errno::ECONNREFUSED, "Unable to connect to Redis on #{host}:#{port}"
378
+ end
379
+
380
+ def connect_to_server
381
+ connect_to(@host, @port)
382
+ call_command([:auth, @password]) if @password
383
+ call_command([:select, @db]) if @db != 0
384
+ @sock
385
+ end
386
+
387
+ def method_missing(*argv)
388
+ call_command(argv)
389
+ end
390
+
391
+ def raw_call_command(argvp)
392
+ if argvp[0].is_a?(Array)
393
+ argvv = argvp
394
+ pipeline = true
395
+ else
396
+ argvv = [argvp]
397
+ pipeline = false
398
+ end
399
+
400
+ if @binary_keys or pipeline or MULTI_BULK_COMMANDS[argvv[0][0].to_s]
401
+ command = ""
402
+ argvv.each do |argv|
403
+ command << "*#{argv.size}\r\n"
404
+ argv.each{|a|
405
+ a = a.to_s
406
+ command << "$#{get_size(a)}\r\n"
407
+ command << a
408
+ command << "\r\n"
409
+ }
410
+ end
411
+ else
412
+ command = ""
413
+ argvv.each do |argv|
414
+ bulk = nil
415
+ argv[0] = argv[0].to_s
416
+ if ALIASES[argv[0]]
417
+ deprecated(argv[0], ALIASES[argv[0]], caller[4])
418
+ argv[0] = ALIASES[argv[0]]
419
+ end
420
+ raise "#{argv[0]} command is disabled" if DISABLED_COMMANDS[argv[0]]
421
+ if BULK_COMMANDS[argv[0]] and argv.length > 1
422
+ bulk = argv[-1].to_s
423
+ argv[-1] = get_size(bulk)
424
+ end
425
+ command << "#{argv.join(' ')}\r\n"
426
+ command << "#{bulk}\r\n" if bulk
427
+ end
428
+ end
429
+ # When in Pub/Sub mode we don't read replies synchronously.
430
+ if @pubsub
431
+ @sock.write(command)
432
+ return true
433
+ end
434
+ # The normal command execution is reading and processing the reply.
435
+ results = maybe_lock do
436
+ begin
437
+ set_socket_timeout!(0) if requires_timeout_reset?(argvv[0][0].to_s)
438
+ process_command(command, argvv)
439
+ ensure
440
+ set_socket_timeout!(@timeout) if requires_timeout_reset?(argvv[0][0].to_s)
441
+ end
442
+ end
443
+
444
+ return pipeline ? results : results[0]
445
+ end
446
+
447
+ def process_command(command, argvv)
448
+ @sock.write(command)
449
+ argvv.map do |argv|
450
+ processor = REPLY_PROCESSOR[argv[0].to_s]
451
+ processor ? processor.call(read_reply) : read_reply
452
+ end
453
+ end
454
+
455
+ def maybe_lock(&block)
456
+ if @thread_safe && !@evented
457
+ @mutex.synchronize(&block)
458
+ else
459
+ block.call
460
+ end
461
+ end
462
+
463
+ def read_reply
464
+
465
+ # We read the first byte using read() mainly because gets() is
466
+ # immune to raw socket timeouts.
467
+ begin
468
+ reply_type = @sock.read(1)
469
+ rescue Errno::EAGAIN
470
+
471
+ # We want to make sure it reconnects on the next command after the
472
+ # timeout. Otherwise the server may reply in the meantime leaving
473
+ # the protocol in a desync status.
474
+ disconnect
475
+
476
+ raise Errno::EAGAIN, "Timeout reading from the socket"
477
+ end
478
+
479
+ raise Errno::ECONNRESET, "Connection lost" unless reply_type
480
+
481
+ format_reply(reply_type, @sock.gets)
482
+ end
483
+
484
+
485
+ if "".respond_to?(:bytesize)
486
+ def get_size(string)
487
+ string.bytesize
488
+ end
489
+ else
490
+ def get_size(string)
491
+ string.size
492
+ end
493
+ end
494
+
495
+ private
496
+
497
+ def log(str, level = :info)
498
+ @logger.send(level, str.to_s) if @logger
499
+ end
500
+
501
+ def deprecated(old, new, trace = caller[0])
502
+ $stderr.puts "\nRedis: The method #{old} is deprecated. Use #{new} instead (in #{trace})"
503
+ end
504
+
505
+ def requires_timeout_reset?(command)
506
+ BLOCKING_COMMANDS[command] && @timeout
507
+ end
508
+
509
+ def set_socket_timeout!(timeout)
510
+ secs = Integer(timeout)
511
+ usecs = Integer((timeout - secs) * 1_000_000)
512
+ optval = [secs, usecs].pack("l_2")
513
+ begin
514
+ @sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval
515
+ @sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval
516
+ rescue Exception => e
517
+ # Solaris, for one, does not like/support socket timeouts.
518
+ log("Unable to use raw socket timeouts: #{e.class.name}: #{e.message}")
519
+ end
520
+ end
521
+
522
+ def connected?
523
+ !! @sock
524
+ end
525
+
526
+ def disconnect
527
+ begin
528
+ @sock.close
529
+ rescue
530
+ ensure
531
+ @sock = nil
532
+ end
533
+ true
534
+ end
535
+
536
+ def reconnect
537
+ disconnect && connect_to_server
538
+ end
539
+
540
+ def format_reply(reply_type, line)
541
+ case reply_type
542
+ when MINUS then format_error_reply(line)
543
+ when PLUS then format_status_reply(line)
544
+ when COLON then format_integer_reply(line)
545
+ when DOLLAR then format_bulk_reply(line)
546
+ when ASTERISK then format_multi_bulk_reply(line)
547
+ else raise ProtocolError.new(reply_type)
548
+ end
549
+ end
550
+
551
+ def format_error_reply(line)
552
+ raise "-" + line.strip
553
+ end
554
+
555
+ def format_status_reply(line)
556
+ line.strip
557
+ end
558
+
559
+ def format_integer_reply(line)
560
+ line.to_i
561
+ end
562
+
563
+ def format_bulk_reply(line)
564
+ bulklen = line.to_i
565
+ return if bulklen == -1
566
+ reply = @sock.read(bulklen)
567
+ @sock.read(2) # Discard CRLF.
568
+ reply
569
+ end
570
+
571
+ def format_multi_bulk_reply(line)
572
+ reply = []
573
+ line.to_i.times { reply << read_reply }
574
+ reply
575
+ end
576
+ end
577
+ end