chimera 0.0.1
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/History.txt +4 -0
- data/Manifest.txt +57 -0
- data/PostInstall.txt +7 -0
- data/README.rdoc +114 -0
- data/Rakefile +30 -0
- data/doc/NOTES +11 -0
- data/doc/examples/config.yml +16 -0
- data/doc/redis6379.conf +132 -0
- data/lib/chimera.rb +33 -0
- data/lib/chimera/associations.rb +146 -0
- data/lib/chimera/attributes.rb +52 -0
- data/lib/chimera/base.rb +95 -0
- data/lib/chimera/config.rb +9 -0
- data/lib/chimera/error.rb +12 -0
- data/lib/chimera/finders.rb +49 -0
- data/lib/chimera/geo_indexes.rb +76 -0
- data/lib/chimera/indexes.rb +177 -0
- data/lib/chimera/persistence.rb +70 -0
- data/lib/chimera/redis_objects.rb +345 -0
- data/lib/redis.rb +373 -0
- data/lib/redis/counter.rb +94 -0
- data/lib/redis/dist_redis.rb +149 -0
- data/lib/redis/hash_ring.rb +135 -0
- data/lib/redis/helpers/core_commands.rb +46 -0
- data/lib/redis/helpers/serialize.rb +25 -0
- data/lib/redis/list.rb +122 -0
- data/lib/redis/lock.rb +83 -0
- data/lib/redis/objects.rb +100 -0
- data/lib/redis/objects/counters.rb +132 -0
- data/lib/redis/objects/lists.rb +45 -0
- data/lib/redis/objects/locks.rb +71 -0
- data/lib/redis/objects/sets.rb +46 -0
- data/lib/redis/objects/values.rb +56 -0
- data/lib/redis/pipeline.rb +21 -0
- data/lib/redis/set.rb +156 -0
- data/lib/redis/value.rb +35 -0
- data/lib/riak_raw.rb +100 -0
- data/lib/typhoeus.rb +55 -0
- data/lib/typhoeus/.gitignore +1 -0
- data/lib/typhoeus/easy.rb +253 -0
- data/lib/typhoeus/filter.rb +28 -0
- data/lib/typhoeus/hydra.rb +210 -0
- data/lib/typhoeus/multi.rb +34 -0
- data/lib/typhoeus/remote.rb +306 -0
- data/lib/typhoeus/remote_method.rb +108 -0
- data/lib/typhoeus/remote_proxy_object.rb +48 -0
- data/lib/typhoeus/request.rb +124 -0
- data/lib/typhoeus/response.rb +39 -0
- data/lib/typhoeus/service.rb +20 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/test/models.rb +49 -0
- data/test/test_chimera.rb +238 -0
- data/test/test_helper.rb +7 -0
- metadata +243 -0
@@ -0,0 +1,70 @@
|
|
1
|
+
module Chimera
|
2
|
+
module Persistence
|
3
|
+
def self.included(base)
|
4
|
+
base.send :extend, ClassMethods
|
5
|
+
base.send :include, InstanceMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
end # ClassMethods
|
10
|
+
|
11
|
+
module InstanceMethods
|
12
|
+
def new?
|
13
|
+
@new == true
|
14
|
+
end
|
15
|
+
|
16
|
+
def save
|
17
|
+
raise(Chimera::Error::SaveWithoutId) unless self.id
|
18
|
+
raise(Chimera::Error::ValidationErrors) unless self.valid?
|
19
|
+
check_index_constraints
|
20
|
+
|
21
|
+
if new?
|
22
|
+
_run_create_callbacks do
|
23
|
+
_run_save_callbacks do
|
24
|
+
save_without_callbacks
|
25
|
+
create_indexes
|
26
|
+
end
|
27
|
+
end
|
28
|
+
else
|
29
|
+
_run_save_callbacks do
|
30
|
+
destroy_indexes
|
31
|
+
save_without_callbacks
|
32
|
+
create_indexes
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
true
|
37
|
+
end
|
38
|
+
|
39
|
+
alias :create :save
|
40
|
+
|
41
|
+
def vector_clock
|
42
|
+
if @riak_response and @riak_response.headers_hash
|
43
|
+
return @riak_response.headers_hash["X-Riak-Vclock"]
|
44
|
+
end; nil
|
45
|
+
end
|
46
|
+
|
47
|
+
def save_without_callbacks
|
48
|
+
@riak_response = self.class.connection(:riak_raw).store(
|
49
|
+
self.class.bucket_key,
|
50
|
+
self.id,
|
51
|
+
YAML.dump(@attributes),
|
52
|
+
self.vector_clock)
|
53
|
+
|
54
|
+
@orig_attributes = @attributes.clone
|
55
|
+
@new = false
|
56
|
+
end
|
57
|
+
|
58
|
+
def destroy
|
59
|
+
_run_destroy_callbacks do
|
60
|
+
@riak_response = self.class.connection(:riak_raw).delete(self.class.bucket_key, self.id)
|
61
|
+
destroy_indexes
|
62
|
+
association_memberships.destroy
|
63
|
+
destroy_associations
|
64
|
+
destroy_redis_objects
|
65
|
+
freeze
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end # InstanceMethods
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,345 @@
|
|
1
|
+
module Chimera
|
2
|
+
module RedisObjects
|
3
|
+
def self.included(base)
|
4
|
+
base.send :extend, ClassMethods
|
5
|
+
base.send :include, InstanceMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def defined_redis_objects
|
10
|
+
@defined_redis_objects || {}
|
11
|
+
end
|
12
|
+
|
13
|
+
# available types include:
|
14
|
+
# string, set, zset, list, counter
|
15
|
+
def redis_object(name, type = :string, extra_opts={})
|
16
|
+
@defined_redis_objects ||= {}
|
17
|
+
@defined_redis_objects[name.to_sym] = [type, extra_opts]
|
18
|
+
define_method("#{name}") do
|
19
|
+
@redis_objects ||= {}
|
20
|
+
case type
|
21
|
+
when :string then
|
22
|
+
@redis_objects[name.to_sym] = Chimera::RedisObjectProxy::String.new(self, name, extra_opts)
|
23
|
+
when :set then
|
24
|
+
@redis_objects[name.to_sym] = Chimera::RedisObjectProxy::Set.new(self, name, extra_opts)
|
25
|
+
when :zset then
|
26
|
+
@redis_objects[name.to_sym] = Chimera::RedisObjectProxy::ZSet.new(self, name, extra_opts)
|
27
|
+
when :list then
|
28
|
+
@redis_objects[name.to_sym] = Chimera::RedisObjectProxy::List.new(self, name, extra_opts)
|
29
|
+
when :counter then
|
30
|
+
@redis_objects[name.to_sym] = Chimera::RedisObjectProxy::Counter.new(self, name, extra_opts)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end # ClassMethods
|
35
|
+
|
36
|
+
module InstanceMethods
|
37
|
+
def destroy_redis_objects
|
38
|
+
(@redis_objects || {}).each do |name, redis_obj|
|
39
|
+
redis_obj.destroy
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end # InstanceMethods
|
43
|
+
end
|
44
|
+
|
45
|
+
module RedisObjectProxy
|
46
|
+
class Base
|
47
|
+
attr_accessor :owner, :name, :extra_opts
|
48
|
+
def initialize(owner, name, extra_opts)
|
49
|
+
unless owner and owner.id
|
50
|
+
raise(Chimera::Errors::MissingId)
|
51
|
+
end
|
52
|
+
|
53
|
+
@owner = owner
|
54
|
+
@name = name
|
55
|
+
@extra_opts = extra_opts
|
56
|
+
end
|
57
|
+
|
58
|
+
def connection
|
59
|
+
self.owner.class.connection(:redis)
|
60
|
+
end
|
61
|
+
|
62
|
+
def key
|
63
|
+
"#{self.class.to_s}::RedisObjects::#{name}::#{self.owner.id}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def destroy
|
67
|
+
connection.del(self.key)
|
68
|
+
end
|
69
|
+
|
70
|
+
def encode(val)
|
71
|
+
YAML.dump(val)
|
72
|
+
end
|
73
|
+
|
74
|
+
def decode(val)
|
75
|
+
return nil if val.nil?
|
76
|
+
return "" if val == ""
|
77
|
+
YAML.load(val)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class Collection < Base
|
82
|
+
def sort(opts={})
|
83
|
+
cmd = [self.key]
|
84
|
+
cmd << "BY #{opts[:by_pattern]}" if opts[:by_pattern]
|
85
|
+
if opts[:limit]
|
86
|
+
start, count = opts[:limit]
|
87
|
+
cmd << "LIMIT #{start} #{count}" if start && count
|
88
|
+
end
|
89
|
+
cmd << "GET #{opts[:get_pattern]}" if opts[:get_pattern]
|
90
|
+
cmd << opts[:order] if opts[:order]
|
91
|
+
cmd << "ALPHA" if opts[:alpha] == true
|
92
|
+
cmd << "STORE #{opts[:dest_key]}" if opts[:dest_key]
|
93
|
+
connection.sort(self.key, cmd.join(" "))
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class String < Base
|
98
|
+
def set(val)
|
99
|
+
connection.set(self.key, encode(val))
|
100
|
+
end
|
101
|
+
|
102
|
+
def get
|
103
|
+
decode(connection.get(self.key))
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class Set < Collection
|
108
|
+
def add(val)
|
109
|
+
connection.sadd(self.key, encode(val))
|
110
|
+
end
|
111
|
+
|
112
|
+
def <<(val)
|
113
|
+
add(val)
|
114
|
+
end
|
115
|
+
|
116
|
+
def rem(val)
|
117
|
+
connection.srem(self.key, encode(val))
|
118
|
+
end
|
119
|
+
|
120
|
+
def pop
|
121
|
+
decode connection.spop(self.key)
|
122
|
+
end
|
123
|
+
|
124
|
+
def move(val, dest_set_key)
|
125
|
+
connection.smove(self.key, dest_set_key, encode(val))
|
126
|
+
end
|
127
|
+
|
128
|
+
def card
|
129
|
+
connection.scard(self.key)
|
130
|
+
end
|
131
|
+
|
132
|
+
alias :size :card
|
133
|
+
alias :count :card
|
134
|
+
|
135
|
+
def ismember(val)
|
136
|
+
connection.sismember(self.key, encode(val))
|
137
|
+
end
|
138
|
+
|
139
|
+
alias :is_member? :ismember
|
140
|
+
alias :include? :ismember
|
141
|
+
alias :includes? :ismember
|
142
|
+
alias :contains? :ismember
|
143
|
+
|
144
|
+
def inter(*set_keys)
|
145
|
+
(connection.sinter(set_keys.join(" ")) || []).collect { |val| decode(val) }
|
146
|
+
end
|
147
|
+
|
148
|
+
alias :intersect :inter
|
149
|
+
|
150
|
+
def interstore(dest_key, *set_keys)
|
151
|
+
connection.sinterstore(dest_key, set_keys.join(" "))
|
152
|
+
end
|
153
|
+
|
154
|
+
alias :intersect_and_store :interstore
|
155
|
+
|
156
|
+
def union(*set_keys)
|
157
|
+
(connection.sunion(set_keys.join(" ")) || []).collect { |val| decode(val) }
|
158
|
+
end
|
159
|
+
|
160
|
+
def unionstore(dest_key, *set_keys)
|
161
|
+
connection.sunionstore(dest_key, set_keys.join(" "))
|
162
|
+
end
|
163
|
+
|
164
|
+
alias :union_and_store :unionstore
|
165
|
+
|
166
|
+
def diff(*set_keys)
|
167
|
+
(connection.sdiff(set_keys.join(" ")) || []).collect { |val| decode(val) }
|
168
|
+
end
|
169
|
+
|
170
|
+
def diffstore(dest_key, *set_keys)
|
171
|
+
connection.sdiffstore(dest_key, set_keys.join(" "))
|
172
|
+
end
|
173
|
+
|
174
|
+
alias :diff_and_store :diffstore
|
175
|
+
|
176
|
+
def members
|
177
|
+
(connection.smembers(self.key) || []).collect { |val| decode(val) }
|
178
|
+
end
|
179
|
+
|
180
|
+
alias :all :members
|
181
|
+
|
182
|
+
def randmember
|
183
|
+
decode connection.srandmember(self.key)
|
184
|
+
end
|
185
|
+
|
186
|
+
alias :rand_member :randmember
|
187
|
+
end
|
188
|
+
|
189
|
+
class ZSet < Collection
|
190
|
+
def add(val,score=0)
|
191
|
+
connection.zadd(self.key, score, encode(val))
|
192
|
+
end
|
193
|
+
|
194
|
+
def rem(val)
|
195
|
+
connection.zrem(self.key, encode(val))
|
196
|
+
end
|
197
|
+
|
198
|
+
def incrby(val, incr)
|
199
|
+
connection.zincrby(self.key, incr.to_f, encode(val))
|
200
|
+
end
|
201
|
+
|
202
|
+
alias :incr_by :incrby
|
203
|
+
|
204
|
+
def range(start_index, end_index, extra_opts={})
|
205
|
+
opts = [self.key, start_index.to_i, end_index.to_i]
|
206
|
+
opts << "WITHSCORES" if extra_opts[:with_scores] == true
|
207
|
+
(connection.zrange(opts) || []).collect { |val| decode(val) }
|
208
|
+
end
|
209
|
+
|
210
|
+
def revrange(start_index, end_index, extra_opts={})
|
211
|
+
opts = [self.key, start_index.to_i, end_index.to_i]
|
212
|
+
opts << "WITHSCORES" if extra_opts[:with_scores] == true
|
213
|
+
(connection.zrevrange(opts) || []).collect { |val| decode(val) }
|
214
|
+
end
|
215
|
+
|
216
|
+
alias :rev_range :revrange
|
217
|
+
|
218
|
+
def rangebyscore(min, max, extra_opts={})
|
219
|
+
opts = [self.key, min.to_f, max.to_f]
|
220
|
+
offset, count = extra_opts[:limit]
|
221
|
+
if offset and count
|
222
|
+
opts << "LIMIT #{offset} #{count}"
|
223
|
+
end
|
224
|
+
opts << "WITHSCORES" if extra_opts[:with_scores] == true
|
225
|
+
(connection.zrangebyscore(opts) || []).collect { |val| decode(val) }
|
226
|
+
end
|
227
|
+
|
228
|
+
alias :range_by_score :rangebyscore
|
229
|
+
|
230
|
+
def remrangebyscore(min,max)
|
231
|
+
connection.zremrangebyscore(self.key,min.to_f,max.to_f)
|
232
|
+
end
|
233
|
+
|
234
|
+
alias :rem_range_by_score :remrangebyscore
|
235
|
+
alias :remove_range_by_score :remrangebyscore
|
236
|
+
|
237
|
+
def card
|
238
|
+
connection.zcard(self.key).to_i
|
239
|
+
end
|
240
|
+
|
241
|
+
alias :size :card
|
242
|
+
alias :count :card
|
243
|
+
|
244
|
+
def score(val)
|
245
|
+
connection.zscore(self.key, val).to_f
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
class List < Collection
|
250
|
+
def rpush(val)
|
251
|
+
connection.rpush(self.key, encode(val))
|
252
|
+
end
|
253
|
+
|
254
|
+
alias :right_push :rpush
|
255
|
+
|
256
|
+
def <<(val)
|
257
|
+
rpush(val)
|
258
|
+
end
|
259
|
+
|
260
|
+
def lpush(val)
|
261
|
+
connection.lpush(self.key, encode(val))
|
262
|
+
end
|
263
|
+
|
264
|
+
alias :left_push :lpush
|
265
|
+
|
266
|
+
def len
|
267
|
+
connection.len(self.key).to_i
|
268
|
+
end
|
269
|
+
|
270
|
+
alias :size :len
|
271
|
+
alias :count :len
|
272
|
+
|
273
|
+
def range(start_index, end_index)
|
274
|
+
(connection.lrange(self.key, start_index.to_i, end_index.to_i) || []).collect { |val| decode(val) }
|
275
|
+
end
|
276
|
+
|
277
|
+
def trim(start_index, end_index)
|
278
|
+
connection.ltrim(self.key, start_index.to_i, end_index.to_i)
|
279
|
+
end
|
280
|
+
|
281
|
+
def index(index)
|
282
|
+
decode connection.lindex(self.key, index.to_i)
|
283
|
+
end
|
284
|
+
|
285
|
+
def [](index)
|
286
|
+
self.index(index)
|
287
|
+
end
|
288
|
+
|
289
|
+
def set(index, val)
|
290
|
+
connection.lset(self.key, index.to_i, encode(val))
|
291
|
+
end
|
292
|
+
|
293
|
+
def rem(val, count=0)
|
294
|
+
connection.lrem(self.key, count.to_i, encode(val))
|
295
|
+
end
|
296
|
+
|
297
|
+
def lpop
|
298
|
+
decode connection.lpop(self.key)
|
299
|
+
end
|
300
|
+
|
301
|
+
alias :left_pop :lpop
|
302
|
+
alias :pop :lpop
|
303
|
+
|
304
|
+
def rpop
|
305
|
+
decode connection.rpop(self.key)
|
306
|
+
end
|
307
|
+
|
308
|
+
alias :right_pop :rpop
|
309
|
+
|
310
|
+
def rpoplpush(dest_key)
|
311
|
+
decode connection.rpoplpush(self.key, dest_key)
|
312
|
+
end
|
313
|
+
|
314
|
+
alias :right_pop_left_push :rpoplpush
|
315
|
+
end
|
316
|
+
|
317
|
+
class Counter < Base
|
318
|
+
def incr
|
319
|
+
connection.incr(self.key).to_i
|
320
|
+
end
|
321
|
+
|
322
|
+
def incrby(val)
|
323
|
+
connection.incrby(self.key,val.to_i).to_i
|
324
|
+
end
|
325
|
+
|
326
|
+
alias :incr_by :incrby
|
327
|
+
|
328
|
+
def decr
|
329
|
+
connection.decr(self.key).to_i
|
330
|
+
end
|
331
|
+
|
332
|
+
def decrby(val)
|
333
|
+
connection.decrby(self.key, val.to_i).to_i
|
334
|
+
end
|
335
|
+
|
336
|
+
def val
|
337
|
+
connection.get(self.key).to_i
|
338
|
+
end
|
339
|
+
|
340
|
+
alias :count :val
|
341
|
+
|
342
|
+
alias :decr_by :decrby
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
data/lib/redis.rb
ADDED
@@ -0,0 +1,373 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require File.join(File.dirname(__FILE__),'redis/pipeline')
|
3
|
+
|
4
|
+
begin
|
5
|
+
if RUBY_VERSION >= '1.9'
|
6
|
+
require 'timeout'
|
7
|
+
RedisTimer = Timeout
|
8
|
+
else
|
9
|
+
require 'system_timer'
|
10
|
+
RedisTimer = SystemTimer
|
11
|
+
end
|
12
|
+
rescue LoadError
|
13
|
+
RedisTimer = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
class Redis
|
17
|
+
OK = "OK".freeze
|
18
|
+
MINUS = "-".freeze
|
19
|
+
PLUS = "+".freeze
|
20
|
+
COLON = ":".freeze
|
21
|
+
DOLLAR = "$".freeze
|
22
|
+
ASTERISK = "*".freeze
|
23
|
+
|
24
|
+
BULK_COMMANDS = {
|
25
|
+
"set" => true,
|
26
|
+
"setnx" => true,
|
27
|
+
"rpush" => true,
|
28
|
+
"lpush" => true,
|
29
|
+
"lset" => true,
|
30
|
+
"lrem" => true,
|
31
|
+
"sadd" => true,
|
32
|
+
"srem" => true,
|
33
|
+
"sismember" => true,
|
34
|
+
"echo" => true,
|
35
|
+
"getset" => true,
|
36
|
+
"smove" => true,
|
37
|
+
"zadd" => true,
|
38
|
+
"zincrby" => true,
|
39
|
+
"zrem" => true,
|
40
|
+
"zscore" => true
|
41
|
+
}
|
42
|
+
|
43
|
+
MULTI_BULK_COMMANDS = {
|
44
|
+
"mset" => true,
|
45
|
+
"msetnx" => 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
|
+
"keys" => lambda{|r| r.split(" ")},
|
64
|
+
"info" => lambda{|r|
|
65
|
+
info = {}
|
66
|
+
r.each_line {|kv|
|
67
|
+
k,v = kv.split(":",2).map{|x| x.chomp}
|
68
|
+
info[k.to_sym] = v
|
69
|
+
}
|
70
|
+
info
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
ALIASES = {
|
75
|
+
"flush_db" => "flushdb",
|
76
|
+
"flush_all" => "flushall",
|
77
|
+
"last_save" => "lastsave",
|
78
|
+
"key?" => "exists",
|
79
|
+
"delete" => "del",
|
80
|
+
"randkey" => "randomkey",
|
81
|
+
"list_length" => "llen",
|
82
|
+
"push_tail" => "rpush",
|
83
|
+
"push_head" => "lpush",
|
84
|
+
"pop_tail" => "rpop",
|
85
|
+
"pop_head" => "lpop",
|
86
|
+
"list_set" => "lset",
|
87
|
+
"list_range" => "lrange",
|
88
|
+
"list_trim" => "ltrim",
|
89
|
+
"list_index" => "lindex",
|
90
|
+
"list_rm" => "lrem",
|
91
|
+
"set_add" => "sadd",
|
92
|
+
"set_delete" => "srem",
|
93
|
+
"set_count" => "scard",
|
94
|
+
"set_member?" => "sismember",
|
95
|
+
"set_members" => "smembers",
|
96
|
+
"set_intersect" => "sinter",
|
97
|
+
"set_intersect_store" => "sinterstore",
|
98
|
+
"set_inter_store" => "sinterstore",
|
99
|
+
"set_union" => "sunion",
|
100
|
+
"set_union_store" => "sunionstore",
|
101
|
+
"set_diff" => "sdiff",
|
102
|
+
"set_diff_store" => "sdiffstore",
|
103
|
+
"set_move" => "smove",
|
104
|
+
"set_unless_exists" => "setnx",
|
105
|
+
"rename_unless_exists" => "renamenx",
|
106
|
+
"type?" => "type",
|
107
|
+
"zset_add" => "zadd",
|
108
|
+
"zset_count" => "zcard",
|
109
|
+
"zset_range_by_score" => "zrangebyscore",
|
110
|
+
"zset_reverse_range" => "zrevrange",
|
111
|
+
"zset_range" => "zrange",
|
112
|
+
"zset_delete" => "zrem",
|
113
|
+
"zset_score" => "zscore",
|
114
|
+
"zset_incr_by" => "zincrby",
|
115
|
+
"zset_increment_by" => "zincrby"
|
116
|
+
}
|
117
|
+
|
118
|
+
DISABLED_COMMANDS = {
|
119
|
+
"monitor" => true,
|
120
|
+
"sync" => true
|
121
|
+
}
|
122
|
+
|
123
|
+
def initialize(options = {})
|
124
|
+
@host = options[:host] || '127.0.0.1'
|
125
|
+
@port = (options[:port] || 6379).to_i
|
126
|
+
@db = (options[:db] || 0).to_i
|
127
|
+
@timeout = (options[:timeout] || 5).to_i
|
128
|
+
@password = options[:password]
|
129
|
+
@logger = options[:logger]
|
130
|
+
@thread_safe = options[:thread_safe]
|
131
|
+
@mutex = Mutex.new if @thread_safe
|
132
|
+
@sock = nil
|
133
|
+
|
134
|
+
@logger.info { self.to_s } if @logger
|
135
|
+
end
|
136
|
+
|
137
|
+
def to_s
|
138
|
+
"Redis Client connected to #{server} against DB #{@db}"
|
139
|
+
end
|
140
|
+
|
141
|
+
def server
|
142
|
+
"#{@host}:#{@port}"
|
143
|
+
end
|
144
|
+
|
145
|
+
def connect_to_server
|
146
|
+
@sock = connect_to(@host, @port, @timeout == 0 ? nil : @timeout)
|
147
|
+
call_command(["auth",@password]) if @password
|
148
|
+
call_command(["select",@db]) unless @db == 0
|
149
|
+
end
|
150
|
+
|
151
|
+
def connect_to(host, port, timeout=nil)
|
152
|
+
# We support connect() timeout only if system_timer is availabe
|
153
|
+
# or if we are running against Ruby >= 1.9
|
154
|
+
# Timeout reading from the socket instead will be supported anyway.
|
155
|
+
if @timeout != 0 and RedisTimer
|
156
|
+
begin
|
157
|
+
sock = TCPSocket.new(host, port)
|
158
|
+
rescue Timeout::Error
|
159
|
+
@sock = nil
|
160
|
+
raise Timeout::Error, "Timeout connecting to the server"
|
161
|
+
end
|
162
|
+
else
|
163
|
+
sock = TCPSocket.new(host, port)
|
164
|
+
end
|
165
|
+
sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
|
166
|
+
|
167
|
+
# If the timeout is set we set the low level socket options in order
|
168
|
+
# to make sure a blocking read will return after the specified number
|
169
|
+
# of seconds. This hack is from memcached ruby client.
|
170
|
+
if timeout
|
171
|
+
secs = Integer(timeout)
|
172
|
+
usecs = Integer((timeout - secs) * 1_000_000)
|
173
|
+
optval = [secs, usecs].pack("l_2")
|
174
|
+
begin
|
175
|
+
sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval
|
176
|
+
sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval
|
177
|
+
rescue Exception => ex
|
178
|
+
# Solaris, for one, does not like/support socket timeouts.
|
179
|
+
@logger.info "Unable to use raw socket timeouts: #{ex.class.name}: #{ex.message}" if @logger
|
180
|
+
end
|
181
|
+
end
|
182
|
+
sock
|
183
|
+
end
|
184
|
+
|
185
|
+
def method_missing(*argv)
|
186
|
+
call_command(argv)
|
187
|
+
end
|
188
|
+
|
189
|
+
def call_command(argv)
|
190
|
+
@logger.debug { argv.inspect } if @logger
|
191
|
+
|
192
|
+
# this wrapper to raw_call_command handle reconnection on socket
|
193
|
+
# error. We try to reconnect just one time, otherwise let the error
|
194
|
+
# araise.
|
195
|
+
connect_to_server if !@sock
|
196
|
+
|
197
|
+
begin
|
198
|
+
raw_call_command(argv.dup)
|
199
|
+
rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED
|
200
|
+
@sock.close rescue nil
|
201
|
+
@sock = nil
|
202
|
+
connect_to_server
|
203
|
+
raw_call_command(argv.dup)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def raw_call_command(argvp)
|
208
|
+
pipeline = argvp[0].is_a?(Array)
|
209
|
+
|
210
|
+
unless pipeline
|
211
|
+
argvv = [argvp]
|
212
|
+
else
|
213
|
+
argvv = argvp
|
214
|
+
end
|
215
|
+
|
216
|
+
if MULTI_BULK_COMMANDS[argvv.flatten[0].to_s]
|
217
|
+
# TODO improve this code
|
218
|
+
argvp = argvv.flatten
|
219
|
+
values = argvp.pop.to_a.flatten
|
220
|
+
argvp = values.unshift(argvp[0])
|
221
|
+
command = ["*#{argvp.size}"]
|
222
|
+
argvp.each do |v|
|
223
|
+
v = v.to_s
|
224
|
+
command << "$#{get_size(v)}"
|
225
|
+
command << v
|
226
|
+
end
|
227
|
+
command = command.map {|cmd| "#{cmd}\r\n"}.join
|
228
|
+
else
|
229
|
+
command = ""
|
230
|
+
argvv.each do |argv|
|
231
|
+
bulk = nil
|
232
|
+
argv[0] = argv[0].to_s.downcase
|
233
|
+
argv[0] = ALIASES[argv[0]] if ALIASES[argv[0]]
|
234
|
+
raise "#{argv[0]} command is disabled" if DISABLED_COMMANDS[argv[0]]
|
235
|
+
if BULK_COMMANDS[argv[0]] and argv.length > 1
|
236
|
+
bulk = argv[-1].to_s
|
237
|
+
argv[-1] = get_size(bulk)
|
238
|
+
end
|
239
|
+
command << "#{argv.join(' ')}\r\n"
|
240
|
+
command << "#{bulk}\r\n" if bulk
|
241
|
+
end
|
242
|
+
end
|
243
|
+
results = maybe_lock { process_command(command, argvv) }
|
244
|
+
|
245
|
+
return pipeline ? results : results[0]
|
246
|
+
end
|
247
|
+
|
248
|
+
def process_command(command, argvv)
|
249
|
+
@sock.write(command)
|
250
|
+
argvv.map do |argv|
|
251
|
+
processor = REPLY_PROCESSOR[argv[0]]
|
252
|
+
processor ? processor.call(read_reply) : read_reply
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def maybe_lock(&block)
|
257
|
+
if @thread_safe
|
258
|
+
@mutex.synchronize(&block)
|
259
|
+
else
|
260
|
+
block.call
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def select(*args)
|
265
|
+
raise "SELECT not allowed, use the :db option when creating the object"
|
266
|
+
end
|
267
|
+
|
268
|
+
def [](key)
|
269
|
+
self.get(key)
|
270
|
+
end
|
271
|
+
|
272
|
+
def []=(key,value)
|
273
|
+
set(key,value)
|
274
|
+
end
|
275
|
+
|
276
|
+
def set(key, value, expiry=nil)
|
277
|
+
s = call_command([:set, key, value]) == OK
|
278
|
+
expire(key, expiry) if s && expiry
|
279
|
+
s
|
280
|
+
end
|
281
|
+
|
282
|
+
def sort(key, options = {})
|
283
|
+
cmd = ["SORT"]
|
284
|
+
cmd << key
|
285
|
+
cmd << "BY #{options[:by]}" if options[:by]
|
286
|
+
cmd << "GET #{[options[:get]].flatten * ' GET '}" if options[:get]
|
287
|
+
cmd << "#{options[:order]}" if options[:order]
|
288
|
+
cmd << "LIMIT #{options[:limit].join(' ')}" if options[:limit]
|
289
|
+
call_command(cmd)
|
290
|
+
end
|
291
|
+
|
292
|
+
def incr(key, increment = nil)
|
293
|
+
call_command(increment ? ["incrby",key,increment] : ["incr",key])
|
294
|
+
end
|
295
|
+
|
296
|
+
def decr(key,decrement = nil)
|
297
|
+
call_command(decrement ? ["decrby",key,decrement] : ["decr",key])
|
298
|
+
end
|
299
|
+
|
300
|
+
# Similar to memcache.rb's #get_multi, returns a hash mapping
|
301
|
+
# keys to values.
|
302
|
+
def mapped_mget(*keys)
|
303
|
+
result = {}
|
304
|
+
mget(*keys).each do |value|
|
305
|
+
key = keys.shift
|
306
|
+
result.merge!(key => value) unless value.nil?
|
307
|
+
end
|
308
|
+
result
|
309
|
+
end
|
310
|
+
|
311
|
+
# Ruby defines a now deprecated type method so we need to override it here
|
312
|
+
# since it will never hit method_missing
|
313
|
+
def type(key)
|
314
|
+
call_command(['type', key])
|
315
|
+
end
|
316
|
+
|
317
|
+
def quit
|
318
|
+
call_command(['quit'])
|
319
|
+
rescue Errno::ECONNRESET
|
320
|
+
end
|
321
|
+
|
322
|
+
def pipelined(&block)
|
323
|
+
pipeline = Pipeline.new self
|
324
|
+
yield pipeline
|
325
|
+
pipeline.execute
|
326
|
+
end
|
327
|
+
|
328
|
+
def read_reply
|
329
|
+
# We read the first byte using read() mainly because gets() is
|
330
|
+
# immune to raw socket timeouts.
|
331
|
+
begin
|
332
|
+
rtype = @sock.read(1)
|
333
|
+
rescue Errno::EAGAIN
|
334
|
+
# We want to make sure it reconnects on the next command after the
|
335
|
+
# timeout. Otherwise the server may reply in the meantime leaving
|
336
|
+
# the protocol in a desync status.
|
337
|
+
@sock = nil
|
338
|
+
raise Errno::EAGAIN, "Timeout reading from the socket"
|
339
|
+
end
|
340
|
+
|
341
|
+
raise Errno::ECONNRESET,"Connection lost" if !rtype
|
342
|
+
line = @sock.gets
|
343
|
+
case rtype
|
344
|
+
when MINUS
|
345
|
+
raise MINUS + line.strip
|
346
|
+
when PLUS
|
347
|
+
line.strip
|
348
|
+
when COLON
|
349
|
+
line.to_i
|
350
|
+
when DOLLAR
|
351
|
+
bulklen = line.to_i
|
352
|
+
return nil if bulklen == -1
|
353
|
+
data = @sock.read(bulklen)
|
354
|
+
@sock.read(2) # CRLF
|
355
|
+
data
|
356
|
+
when ASTERISK
|
357
|
+
objects = line.to_i
|
358
|
+
return nil if bulklen == -1
|
359
|
+
res = []
|
360
|
+
objects.times {
|
361
|
+
res << read_reply
|
362
|
+
}
|
363
|
+
res
|
364
|
+
else
|
365
|
+
raise "Protocol error, got '#{rtype}' as initial reply byte"
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
private
|
370
|
+
def get_size(string)
|
371
|
+
string.respond_to?(:bytesize) ? string.bytesize : string.size
|
372
|
+
end
|
373
|
+
end
|