chimera 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/History.txt +4 -0
  2. data/Manifest.txt +57 -0
  3. data/PostInstall.txt +7 -0
  4. data/README.rdoc +114 -0
  5. data/Rakefile +30 -0
  6. data/doc/NOTES +11 -0
  7. data/doc/examples/config.yml +16 -0
  8. data/doc/redis6379.conf +132 -0
  9. data/lib/chimera.rb +33 -0
  10. data/lib/chimera/associations.rb +146 -0
  11. data/lib/chimera/attributes.rb +52 -0
  12. data/lib/chimera/base.rb +95 -0
  13. data/lib/chimera/config.rb +9 -0
  14. data/lib/chimera/error.rb +12 -0
  15. data/lib/chimera/finders.rb +49 -0
  16. data/lib/chimera/geo_indexes.rb +76 -0
  17. data/lib/chimera/indexes.rb +177 -0
  18. data/lib/chimera/persistence.rb +70 -0
  19. data/lib/chimera/redis_objects.rb +345 -0
  20. data/lib/redis.rb +373 -0
  21. data/lib/redis/counter.rb +94 -0
  22. data/lib/redis/dist_redis.rb +149 -0
  23. data/lib/redis/hash_ring.rb +135 -0
  24. data/lib/redis/helpers/core_commands.rb +46 -0
  25. data/lib/redis/helpers/serialize.rb +25 -0
  26. data/lib/redis/list.rb +122 -0
  27. data/lib/redis/lock.rb +83 -0
  28. data/lib/redis/objects.rb +100 -0
  29. data/lib/redis/objects/counters.rb +132 -0
  30. data/lib/redis/objects/lists.rb +45 -0
  31. data/lib/redis/objects/locks.rb +71 -0
  32. data/lib/redis/objects/sets.rb +46 -0
  33. data/lib/redis/objects/values.rb +56 -0
  34. data/lib/redis/pipeline.rb +21 -0
  35. data/lib/redis/set.rb +156 -0
  36. data/lib/redis/value.rb +35 -0
  37. data/lib/riak_raw.rb +100 -0
  38. data/lib/typhoeus.rb +55 -0
  39. data/lib/typhoeus/.gitignore +1 -0
  40. data/lib/typhoeus/easy.rb +253 -0
  41. data/lib/typhoeus/filter.rb +28 -0
  42. data/lib/typhoeus/hydra.rb +210 -0
  43. data/lib/typhoeus/multi.rb +34 -0
  44. data/lib/typhoeus/remote.rb +306 -0
  45. data/lib/typhoeus/remote_method.rb +108 -0
  46. data/lib/typhoeus/remote_proxy_object.rb +48 -0
  47. data/lib/typhoeus/request.rb +124 -0
  48. data/lib/typhoeus/response.rb +39 -0
  49. data/lib/typhoeus/service.rb +20 -0
  50. data/script/console +10 -0
  51. data/script/destroy +14 -0
  52. data/script/generate +14 -0
  53. data/test/models.rb +49 -0
  54. data/test/test_chimera.rb +238 -0
  55. data/test/test_helper.rb +7 -0
  56. 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
@@ -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