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.
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