dsander-redis 1.0.6
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/LICENSE +20 -0
- data/README.markdown +112 -0
- data/Rakefile +75 -0
- data/lib/edis.rb +3 -0
- data/lib/redis.rb +25 -0
- data/lib/redis/client.rb +577 -0
- data/lib/redis/dist_redis.rb +118 -0
- data/lib/redis/event_machine.rb +181 -0
- data/lib/redis/hash_ring.rb +131 -0
- data/lib/redis/pipeline.rb +19 -0
- data/lib/redis/raketasks.rb +1 -0
- data/lib/redis/subscribe.rb +16 -0
- data/tasks/redis.tasks.rb +140 -0
- metadata +80 -0
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
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'
|
data/lib/redis/client.rb
ADDED
@@ -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
|