redis 0.1.2 → 1.0.0
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.
- data/README.markdown +64 -26
- data/Rakefile +8 -16
- data/lib/edis.rb +3 -0
- data/lib/redis/client.rb +482 -0
- data/lib/redis/dist_redis.rb +114 -0
- data/lib/redis/hash_ring.rb +131 -0
- data/lib/{pipeline.rb → redis/pipeline.rb} +3 -4
- data/lib/redis.rb +13 -362
- metadata +23 -14
- data/lib/dist_redis.rb +0 -149
- data/lib/hash_ring.rb +0 -135
- data/spec/redis_spec.rb +0 -668
- data/spec/spec_helper.rb +0 -4
data/README.markdown
CHANGED
|
@@ -1,36 +1,74 @@
|
|
|
1
1
|
# redis-rb
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A Ruby client library for the [Redis](http://code.google.com/p/redis) key-value storage system.
|
|
4
4
|
|
|
5
|
-
## Information about
|
|
5
|
+
## Information about Redis
|
|
6
6
|
|
|
7
|
-
Redis is a key
|
|
8
|
-
1. It's fast.
|
|
9
|
-
2. Keys are strings but values can have types of "NONE", "STRING", "LIST", or "SET". List's can be atomically push'd, pop'd, lpush'd, lpop'd and indexed. This allows you to store things like lists of comments under one key while retaining the ability to append comments without reading and putting back the whole list.
|
|
10
|
-
|
|
11
|
-
See [redis on code.google.com](http://code.google.com/p/redis/wiki/README) for more information.
|
|
12
|
-
|
|
13
|
-
See the build on [RunCodeRun](http://runcoderun.com/rsanheim/redis-rb)
|
|
7
|
+
Redis is a key-value store with some interesting features:
|
|
14
8
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
For all types redis-rb needs redis-server running to connect to.
|
|
17
|
+
|
|
18
|
+
### Simple Key Value Strings can be used like a large Ruby Hash (Similar to Memcached, Tokyo Cabinet)
|
|
19
|
+
|
|
20
|
+
require 'redis'
|
|
21
|
+
r = Redis.new
|
|
22
|
+
r['key_one'] = "value_one"
|
|
23
|
+
r['key_two'] = "value_two"
|
|
24
|
+
|
|
25
|
+
r['key_one] # => "value_one"
|
|
26
|
+
|
|
27
|
+
### Redis only stores strings. To store Objects, Array or Hashes, you must [Marshal](http://ruby-doc.org/core/classes/Marshal.html)
|
|
28
|
+
|
|
29
|
+
require 'redis'
|
|
30
|
+
r = Redis.new
|
|
31
|
+
|
|
32
|
+
example_hash_to_store = {:name => "Alex", :age => 21, :password => "letmein", :cool => false}
|
|
33
|
+
|
|
34
|
+
r['key_one'] = Marshal.dump(example_hash_to_store)
|
|
35
|
+
|
|
36
|
+
hash_returned_from_redis = Marshal.load(r['key_one'])
|
|
37
|
+
|
|
38
|
+
### Alternatively you can use the [Redis Commands](http://code.google.com/p/redis/wiki/CommandReference)
|
|
39
|
+
|
|
40
|
+
require 'redis'
|
|
41
|
+
r = Redis.new
|
|
42
|
+
r.set 'key_one', 'value_one'
|
|
43
|
+
r.get 'key_one' # => 'value_one'
|
|
44
|
+
|
|
45
|
+
# Using Redis list objects
|
|
46
|
+
# Push an object to the head of the list. Creates the list if it doesn't allready exsist.
|
|
47
|
+
|
|
48
|
+
blog_hash = {:title => "Redis Rules!", :body => "Ok so, like why, well like, RDBMS is like....", :created_at => Time.now.to_i}
|
|
49
|
+
r.lpush 'all_blogs', Marshal.dump(blog_hash)
|
|
50
|
+
|
|
51
|
+
# Get a range of strings from the all_blogs list. Similar to offset and limit in SQL (-1, means the last one)
|
|
52
|
+
|
|
53
|
+
r.lrange 'all_blogs', 0, -1
|
|
54
|
+
|
|
55
|
+
### Multiple commands at once!
|
|
56
|
+
|
|
57
|
+
require 'redis'
|
|
58
|
+
r = Redis.new
|
|
59
|
+
r.multi do
|
|
60
|
+
r.set 'foo', 'bar'
|
|
61
|
+
r.incr 'baz'
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
## Contributing
|
|
65
|
+
|
|
66
|
+
See the build on [RunCodeRun](http://runcoderun.com/rsanheim/redis-rb).
|
|
67
|
+
|
|
68
|
+
If you would like to submit patches, you'll need Redis in your development environment:
|
|
21
69
|
|
|
22
70
|
rake redis:install
|
|
23
71
|
|
|
24
|
-
2. dtach -
|
|
25
|
-
|
|
26
|
-
rake dtach:install
|
|
27
|
-
|
|
28
|
-
3. git - git is the new black.
|
|
29
|
-
|
|
30
|
-
## Setup
|
|
31
|
-
|
|
32
|
-
Use the tasks mentioned above (in Dependencies) to get your machine setup.
|
|
33
|
-
|
|
34
72
|
## Examples
|
|
35
73
|
|
|
36
|
-
Check the examples
|
|
74
|
+
Check the `examples/` directory. You'll need to have an instance of `redis-server` running before running the examples.
|
data/Rakefile
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
require 'rubygems'
|
|
2
2
|
require 'rake/gempackagetask'
|
|
3
|
-
require '
|
|
4
|
-
require 'date'
|
|
5
|
-
require 'spec/rake/spectask'
|
|
3
|
+
require 'rake/testtask'
|
|
6
4
|
require 'tasks/redis.tasks'
|
|
7
5
|
|
|
6
|
+
$:.unshift File.join(File.dirname(__FILE__), 'lib')
|
|
7
|
+
require 'redis'
|
|
8
8
|
|
|
9
9
|
GEM = 'redis'
|
|
10
10
|
GEM_NAME = 'redis'
|
|
11
|
-
GEM_VERSION =
|
|
11
|
+
GEM_VERSION = Redis::VERSION
|
|
12
12
|
AUTHORS = ['Ezra Zygmuntowicz', 'Taylor Weibley', 'Matthew Clark', 'Brian McKinney', 'Salvatore Sanfilippo', 'Luca Guidi']
|
|
13
13
|
EMAIL = "ez@engineyard.com"
|
|
14
14
|
HOMEPAGE = "http://github.com/ezmobius/redis-rb"
|
|
@@ -31,12 +31,10 @@ spec = Gem::Specification.new do |s|
|
|
|
31
31
|
s.files = %w(LICENSE README.markdown Rakefile) + Dir.glob("{lib,tasks,spec}/**/*")
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
task :default => :
|
|
34
|
+
task :default => :test
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
t.spec_files = FileList['spec/**/*_spec.rb']
|
|
39
|
-
t.spec_opts = %w(-fs --color)
|
|
36
|
+
Rake::TestTask.new(:test) do |t|
|
|
37
|
+
t.pattern = 'test/**/*_test.rb'
|
|
40
38
|
end
|
|
41
39
|
|
|
42
40
|
Rake::GemPackageTask.new(spec) do |pkg|
|
|
@@ -49,14 +47,8 @@ task :install => [:package] do
|
|
|
49
47
|
end
|
|
50
48
|
|
|
51
49
|
desc "create a gemspec file"
|
|
52
|
-
task :
|
|
50
|
+
task :gemspec do
|
|
53
51
|
File.open("#{GEM}.gemspec", "w") do |file|
|
|
54
52
|
file.puts spec.to_ruby
|
|
55
53
|
end
|
|
56
54
|
end
|
|
57
|
-
|
|
58
|
-
desc "Run all examples with RCov"
|
|
59
|
-
Spec::Rake::SpecTask.new(:rcov) do |t|
|
|
60
|
-
t.spec_files = FileList['spec/**/*_spec.rb']
|
|
61
|
-
t.rcov = true
|
|
62
|
-
end
|
data/lib/edis.rb
ADDED
data/lib/redis/client.rb
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
class Redis
|
|
2
|
+
class Client
|
|
3
|
+
OK = "OK".freeze
|
|
4
|
+
MINUS = "-".freeze
|
|
5
|
+
PLUS = "+".freeze
|
|
6
|
+
COLON = ":".freeze
|
|
7
|
+
DOLLAR = "$".freeze
|
|
8
|
+
ASTERISK = "*".freeze
|
|
9
|
+
|
|
10
|
+
BULK_COMMANDS = {
|
|
11
|
+
"set" => true,
|
|
12
|
+
"setnx" => true,
|
|
13
|
+
"rpush" => true,
|
|
14
|
+
"lpush" => true,
|
|
15
|
+
"lset" => true,
|
|
16
|
+
"lrem" => true,
|
|
17
|
+
"sadd" => true,
|
|
18
|
+
"srem" => true,
|
|
19
|
+
"sismember" => true,
|
|
20
|
+
"echo" => true,
|
|
21
|
+
"getset" => true,
|
|
22
|
+
"smove" => true,
|
|
23
|
+
"zadd" => true,
|
|
24
|
+
"zincrby" => true,
|
|
25
|
+
"zrem" => true,
|
|
26
|
+
"zscore" => true,
|
|
27
|
+
"zrank" => true,
|
|
28
|
+
"zrevrank" => true,
|
|
29
|
+
"hget" => true,
|
|
30
|
+
"hdel" => true,
|
|
31
|
+
"hexists" => true,
|
|
32
|
+
"publish" => true
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
MULTI_BULK_COMMANDS = {
|
|
36
|
+
"mset" => true,
|
|
37
|
+
"msetnx" => true,
|
|
38
|
+
"hset" => true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
BOOLEAN_PROCESSOR = lambda{|r| r == 1 }
|
|
42
|
+
|
|
43
|
+
REPLY_PROCESSOR = {
|
|
44
|
+
"exists" => BOOLEAN_PROCESSOR,
|
|
45
|
+
"sismember" => BOOLEAN_PROCESSOR,
|
|
46
|
+
"sadd" => BOOLEAN_PROCESSOR,
|
|
47
|
+
"srem" => BOOLEAN_PROCESSOR,
|
|
48
|
+
"smove" => BOOLEAN_PROCESSOR,
|
|
49
|
+
"zadd" => BOOLEAN_PROCESSOR,
|
|
50
|
+
"zrem" => BOOLEAN_PROCESSOR,
|
|
51
|
+
"move" => BOOLEAN_PROCESSOR,
|
|
52
|
+
"setnx" => BOOLEAN_PROCESSOR,
|
|
53
|
+
"del" => BOOLEAN_PROCESSOR,
|
|
54
|
+
"renamenx" => BOOLEAN_PROCESSOR,
|
|
55
|
+
"expire" => BOOLEAN_PROCESSOR,
|
|
56
|
+
"hset" => BOOLEAN_PROCESSOR,
|
|
57
|
+
"hexists" => BOOLEAN_PROCESSOR,
|
|
58
|
+
"info" => lambda{|r|
|
|
59
|
+
info = {}
|
|
60
|
+
r.each_line {|kv|
|
|
61
|
+
k,v = kv.split(":",2).map{|x| x.chomp}
|
|
62
|
+
info[k.to_sym] = v
|
|
63
|
+
}
|
|
64
|
+
info
|
|
65
|
+
},
|
|
66
|
+
"keys" => lambda{|r|
|
|
67
|
+
if r.is_a?(Array)
|
|
68
|
+
r
|
|
69
|
+
else
|
|
70
|
+
r.split(" ")
|
|
71
|
+
end
|
|
72
|
+
},
|
|
73
|
+
"hgetall" => lambda{|r|
|
|
74
|
+
Hash[*r]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
ALIASES = {
|
|
79
|
+
"flush_db" => "flushdb",
|
|
80
|
+
"flush_all" => "flushall",
|
|
81
|
+
"last_save" => "lastsave",
|
|
82
|
+
"key?" => "exists",
|
|
83
|
+
"delete" => "del",
|
|
84
|
+
"randkey" => "randomkey",
|
|
85
|
+
"list_length" => "llen",
|
|
86
|
+
"push_tail" => "rpush",
|
|
87
|
+
"push_head" => "lpush",
|
|
88
|
+
"pop_tail" => "rpop",
|
|
89
|
+
"pop_head" => "lpop",
|
|
90
|
+
"list_set" => "lset",
|
|
91
|
+
"list_range" => "lrange",
|
|
92
|
+
"list_trim" => "ltrim",
|
|
93
|
+
"list_index" => "lindex",
|
|
94
|
+
"list_rm" => "lrem",
|
|
95
|
+
"set_add" => "sadd",
|
|
96
|
+
"set_delete" => "srem",
|
|
97
|
+
"set_count" => "scard",
|
|
98
|
+
"set_member?" => "sismember",
|
|
99
|
+
"set_members" => "smembers",
|
|
100
|
+
"set_intersect" => "sinter",
|
|
101
|
+
"set_intersect_store" => "sinterstore",
|
|
102
|
+
"set_inter_store" => "sinterstore",
|
|
103
|
+
"set_union" => "sunion",
|
|
104
|
+
"set_union_store" => "sunionstore",
|
|
105
|
+
"set_diff" => "sdiff",
|
|
106
|
+
"set_diff_store" => "sdiffstore",
|
|
107
|
+
"set_move" => "smove",
|
|
108
|
+
"set_unless_exists" => "setnx",
|
|
109
|
+
"rename_unless_exists" => "renamenx",
|
|
110
|
+
"type?" => "type",
|
|
111
|
+
"zset_add" => "zadd",
|
|
112
|
+
"zset_count" => "zcard",
|
|
113
|
+
"zset_range_by_score" => "zrangebyscore",
|
|
114
|
+
"zset_reverse_range" => "zrevrange",
|
|
115
|
+
"zset_range" => "zrange",
|
|
116
|
+
"zset_delete" => "zrem",
|
|
117
|
+
"zset_score" => "zscore",
|
|
118
|
+
"zset_incr_by" => "zincrby",
|
|
119
|
+
"zset_increment_by" => "zincrby"
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
DISABLED_COMMANDS = {
|
|
123
|
+
"monitor" => true,
|
|
124
|
+
"sync" => true
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
def initialize(options = {})
|
|
128
|
+
@host = options[:host] || '127.0.0.1'
|
|
129
|
+
@port = (options[:port] || 6379).to_i
|
|
130
|
+
@db = (options[:db] || 0).to_i
|
|
131
|
+
@timeout = (options[:timeout] || 5).to_i
|
|
132
|
+
@password = options[:password]
|
|
133
|
+
@logger = options[:logger]
|
|
134
|
+
@thread_safe = options[:thread_safe]
|
|
135
|
+
@binary_keys = options[:binary_keys]
|
|
136
|
+
@mutex = Mutex.new if @thread_safe
|
|
137
|
+
@sock = nil
|
|
138
|
+
@pubsub = false
|
|
139
|
+
|
|
140
|
+
log(self)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def to_s
|
|
144
|
+
"Redis Client connected to #{server} against DB #{@db}"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def select(*args)
|
|
148
|
+
raise "SELECT not allowed, use the :db option when creating the object"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def [](key)
|
|
152
|
+
get(key)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def []=(key,value)
|
|
156
|
+
set(key, value)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def set(key, value, ttl = nil)
|
|
160
|
+
if ttl
|
|
161
|
+
deprecated("set with an expire", :set_with_expire, caller[0])
|
|
162
|
+
set_with_expire(key, value, ttl)
|
|
163
|
+
else
|
|
164
|
+
call_command([:set, key, value])
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def set_with_expire(key, value, ttl)
|
|
169
|
+
multi do
|
|
170
|
+
set(key, value)
|
|
171
|
+
expire(key, ttl)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def mset(*args)
|
|
176
|
+
if args.size == 1
|
|
177
|
+
deprecated("mset with a hash", :mapped_mset, caller[0])
|
|
178
|
+
mapped_mset(args[0])
|
|
179
|
+
else
|
|
180
|
+
call_command(args.unshift(:mset))
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def mapped_mset(hash)
|
|
185
|
+
mset(*hash.to_a.flatten)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def msetnx(*args)
|
|
189
|
+
if args.size == 1
|
|
190
|
+
deprecated("msetnx with a hash", :mapped_msetnx, caller[0])
|
|
191
|
+
mapped_msetnx(args[0])
|
|
192
|
+
else
|
|
193
|
+
call_command(args.unshift(:msetnx))
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def mapped_msetnx(hash)
|
|
198
|
+
msetnx(*hash.to_a.flatten)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Similar to memcache.rb's #get_multi, returns a hash mapping
|
|
202
|
+
# keys to values.
|
|
203
|
+
def mapped_mget(*keys)
|
|
204
|
+
result = {}
|
|
205
|
+
mget(*keys).each do |value|
|
|
206
|
+
key = keys.shift
|
|
207
|
+
result.merge!(key => value) unless value.nil?
|
|
208
|
+
end
|
|
209
|
+
result
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def sort(key, options = {})
|
|
213
|
+
cmd = ["SORT"]
|
|
214
|
+
cmd << key
|
|
215
|
+
cmd << "BY #{options[:by]}" if options[:by]
|
|
216
|
+
cmd << "GET #{[options[:get]].flatten * ' GET '}" if options[:get]
|
|
217
|
+
cmd << "#{options[:order]}" if options[:order]
|
|
218
|
+
cmd << "LIMIT #{options[:limit].join(' ')}" if options[:limit]
|
|
219
|
+
cmd << "STORE #{options[:store]}" if options[:store]
|
|
220
|
+
call_command(cmd)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def incr(key, increment = nil)
|
|
224
|
+
call_command(increment ? ["incrby",key,increment] : ["incr",key])
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def decr(key,decrement = nil)
|
|
228
|
+
call_command(decrement ? ["decrby",key,decrement] : ["decr",key])
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Ruby defines a now deprecated type method so we need to override it here
|
|
232
|
+
# since it will never hit method_missing
|
|
233
|
+
def type(key)
|
|
234
|
+
call_command(['type', key])
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def quit
|
|
238
|
+
call_command(['quit'])
|
|
239
|
+
rescue Errno::ECONNRESET
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def pipelined(&block)
|
|
243
|
+
pipeline = Pipeline.new self
|
|
244
|
+
yield pipeline
|
|
245
|
+
pipeline.execute
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def exec
|
|
249
|
+
# Need to override Kernel#exec.
|
|
250
|
+
call_command([:exec])
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def multi(&block)
|
|
254
|
+
result = call_command [:multi]
|
|
255
|
+
|
|
256
|
+
return result unless block_given?
|
|
257
|
+
|
|
258
|
+
begin
|
|
259
|
+
yield(self)
|
|
260
|
+
exec
|
|
261
|
+
rescue Exception => e
|
|
262
|
+
discard
|
|
263
|
+
raise e
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def subscribe(*classes)
|
|
268
|
+
# Sanity check, as our API is a bit tricky. You MUST call
|
|
269
|
+
# the top-level subscribe with a block, but you can NOT call
|
|
270
|
+
# the nested subscribe calls with a block, as all the messages
|
|
271
|
+
# are processed by the top level call.
|
|
272
|
+
if @pubsub == false and !block_given?
|
|
273
|
+
raise "You must pass a block to the top level subscribe call"
|
|
274
|
+
elsif @pubsub == true and block_given?
|
|
275
|
+
raise "Can't pass a block to nested subscribe calls"
|
|
276
|
+
elsif @pubsub == true
|
|
277
|
+
# This is a nested subscribe call without a block given.
|
|
278
|
+
# We just need to send the subscribe command and return asap.
|
|
279
|
+
call_command [:subscribe,*classes]
|
|
280
|
+
return true
|
|
281
|
+
end
|
|
282
|
+
@pubsub = true
|
|
283
|
+
call_command [:subscribe,*classes]
|
|
284
|
+
while true
|
|
285
|
+
r = read_reply
|
|
286
|
+
msg = {:type => r[0], :class => r[1], :data => r[2]}
|
|
287
|
+
yield(msg)
|
|
288
|
+
break if msg[:type] == "unsubscribe" and r[2] == 0
|
|
289
|
+
end
|
|
290
|
+
@pubsub = false
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def unsubscribe(*classes)
|
|
294
|
+
call_command [:unsubscribe,*classes]
|
|
295
|
+
return true
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
protected
|
|
299
|
+
|
|
300
|
+
def call_command(argv)
|
|
301
|
+
log(argv.inspect, :debug)
|
|
302
|
+
|
|
303
|
+
# this wrapper to raw_call_command handle reconnection on socket
|
|
304
|
+
# error. We try to reconnect just one time, otherwise let the error
|
|
305
|
+
# araise.
|
|
306
|
+
connect_to_server if !@sock
|
|
307
|
+
|
|
308
|
+
begin
|
|
309
|
+
raw_call_command(argv.dup)
|
|
310
|
+
rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED
|
|
311
|
+
@sock.close rescue nil
|
|
312
|
+
@sock = nil
|
|
313
|
+
connect_to_server
|
|
314
|
+
raw_call_command(argv.dup)
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def server
|
|
319
|
+
"#{@host}:#{@port}"
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def connect_to(host, port, timeout=nil)
|
|
323
|
+
# We support connect() timeout only if system_timer is availabe
|
|
324
|
+
# or if we are running against Ruby >= 1.9
|
|
325
|
+
# Timeout reading from the socket instead will be supported anyway.
|
|
326
|
+
if @timeout != 0 and Timer
|
|
327
|
+
begin
|
|
328
|
+
sock = TCPSocket.new(host, port)
|
|
329
|
+
rescue Timeout::Error
|
|
330
|
+
@sock = nil
|
|
331
|
+
raise Timeout::Error, "Timeout connecting to the server"
|
|
332
|
+
end
|
|
333
|
+
else
|
|
334
|
+
sock = TCPSocket.new(host, port)
|
|
335
|
+
end
|
|
336
|
+
sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
|
|
337
|
+
|
|
338
|
+
# If the timeout is set we set the low level socket options in order
|
|
339
|
+
# to make sure a blocking read will return after the specified number
|
|
340
|
+
# of seconds. This hack is from memcached ruby client.
|
|
341
|
+
if timeout
|
|
342
|
+
secs = Integer(timeout)
|
|
343
|
+
usecs = Integer((timeout - secs) * 1_000_000)
|
|
344
|
+
optval = [secs, usecs].pack("l_2")
|
|
345
|
+
begin
|
|
346
|
+
sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval
|
|
347
|
+
sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval
|
|
348
|
+
rescue Exception => ex
|
|
349
|
+
# Solaris, for one, does not like/support socket timeouts.
|
|
350
|
+
log("Unable to use raw socket timeouts: #{ex.class.name}: #{ex.message}")
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
sock
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def connect_to_server
|
|
357
|
+
@sock = connect_to(@host, @port, @timeout == 0 ? nil : @timeout)
|
|
358
|
+
call_command(["auth",@password]) if @password
|
|
359
|
+
call_command(["select",@db]) unless @db == 0
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def method_missing(*argv)
|
|
363
|
+
call_command(argv)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def raw_call_command(argvp)
|
|
367
|
+
if argvp[0].is_a?(Array)
|
|
368
|
+
argvv = argvp
|
|
369
|
+
pipeline = true
|
|
370
|
+
else
|
|
371
|
+
argvv = [argvp]
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
if @binary_keys or MULTI_BULK_COMMANDS[argvv[0][0].to_s]
|
|
375
|
+
command = ""
|
|
376
|
+
argvv.each do |argv|
|
|
377
|
+
command << "*#{argv.size}\r\n"
|
|
378
|
+
argv.each{|a|
|
|
379
|
+
a = a.to_s
|
|
380
|
+
command << "$#{get_size(a)}\r\n"
|
|
381
|
+
command << a
|
|
382
|
+
command << "\r\n"
|
|
383
|
+
}
|
|
384
|
+
end
|
|
385
|
+
else
|
|
386
|
+
command = ""
|
|
387
|
+
argvv.each do |argv|
|
|
388
|
+
bulk = nil
|
|
389
|
+
argv[0] = argv[0].to_s
|
|
390
|
+
if ALIASES[argv[0]]
|
|
391
|
+
deprecated(argv[0], ALIASES[argv[0]], caller[4])
|
|
392
|
+
argv[0] = ALIASES[argv[0]]
|
|
393
|
+
end
|
|
394
|
+
raise "#{argv[0]} command is disabled" if DISABLED_COMMANDS[argv[0]]
|
|
395
|
+
if BULK_COMMANDS[argv[0]] and argv.length > 1
|
|
396
|
+
bulk = argv[-1].to_s
|
|
397
|
+
argv[-1] = get_size(bulk)
|
|
398
|
+
end
|
|
399
|
+
command << "#{argv.join(' ')}\r\n"
|
|
400
|
+
command << "#{bulk}\r\n" if bulk
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
# When in Pub/Sub mode we don't read replies synchronously.
|
|
404
|
+
if @pubsub
|
|
405
|
+
@sock.write(command)
|
|
406
|
+
return true
|
|
407
|
+
end
|
|
408
|
+
# The normal command execution is reading and processing the reply.
|
|
409
|
+
results = maybe_lock { process_command(command, argvv) }
|
|
410
|
+
return pipeline ? results : results[0]
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def process_command(command, argvv)
|
|
414
|
+
@sock.write(command)
|
|
415
|
+
argvv.map do |argv|
|
|
416
|
+
processor = REPLY_PROCESSOR[argv[0].to_s]
|
|
417
|
+
processor ? processor.call(read_reply) : read_reply
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def maybe_lock(&block)
|
|
422
|
+
if @thread_safe
|
|
423
|
+
@mutex.synchronize(&block)
|
|
424
|
+
else
|
|
425
|
+
block.call
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def read_reply
|
|
430
|
+
# We read the first byte using read() mainly because gets() is
|
|
431
|
+
# immune to raw socket timeouts.
|
|
432
|
+
begin
|
|
433
|
+
rtype = @sock.read(1)
|
|
434
|
+
rescue Errno::EAGAIN
|
|
435
|
+
# We want to make sure it reconnects on the next command after the
|
|
436
|
+
# timeout. Otherwise the server may reply in the meantime leaving
|
|
437
|
+
# the protocol in a desync status.
|
|
438
|
+
@sock = nil
|
|
439
|
+
raise Errno::EAGAIN, "Timeout reading from the socket"
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
raise Errno::ECONNRESET,"Connection lost" if !rtype
|
|
443
|
+
line = @sock.gets
|
|
444
|
+
case rtype
|
|
445
|
+
when MINUS
|
|
446
|
+
raise MINUS + line.strip
|
|
447
|
+
when PLUS
|
|
448
|
+
line.strip
|
|
449
|
+
when COLON
|
|
450
|
+
line.to_i
|
|
451
|
+
when DOLLAR
|
|
452
|
+
bulklen = line.to_i
|
|
453
|
+
return nil if bulklen == -1
|
|
454
|
+
data = @sock.read(bulklen)
|
|
455
|
+
@sock.read(2) # CRLF
|
|
456
|
+
data
|
|
457
|
+
when ASTERISK
|
|
458
|
+
objects = line.to_i
|
|
459
|
+
return nil if bulklen == -1
|
|
460
|
+
res = []
|
|
461
|
+
objects.times {
|
|
462
|
+
res << read_reply
|
|
463
|
+
}
|
|
464
|
+
res
|
|
465
|
+
else
|
|
466
|
+
raise "Protocol error, got '#{rtype}' as initial reply byte"
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def get_size(string)
|
|
471
|
+
string.respond_to?(:bytesize) ? string.bytesize : string.size
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def log(str, level = :info)
|
|
475
|
+
@logger.send(level, str.to_s) if @logger
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def deprecated(old, new, trace = caller[0])
|
|
479
|
+
$stderr.puts "\nRedis: The method #{old} is deprecated. Use #{new} instead (in #{trace})"
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
end
|