joffice_redis 0.1.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.
@@ -0,0 +1,121 @@
1
+ #encoding: utf-8
2
+ =begin
3
+ Copyright 2010 Denis Kokorin <virkdi@mail.ru>
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+ =end
17
+
18
+
19
+ require 'joffice/kernel/array'
20
+
21
+ module JOffice
22
+ class RedisClientBase
23
+
24
+ DEFAULT_KEYS_TO_SAVE= [:id].freeze
25
+
26
+ def [](k)
27
+ db.get(key(k))
28
+ end
29
+
30
+ def []=(k, value)
31
+ db.set!(key(k), value)
32
+ end
33
+
34
+ def exists?(k)
35
+ db.exists?(key(k))
36
+ end
37
+
38
+ def delete_all
39
+ db.del!(db.keys("#{prefix}*"))
40
+ end
41
+
42
+ def db_name
43
+ raise "Missed db name in class #{self}"
44
+ end
45
+
46
+ def db
47
+ @db||=JOffice::RedisManager.instance.open_db_em(db_name)
48
+ end
49
+
50
+ def prefix
51
+ ''
52
+ end
53
+
54
+ def key(hash_key)
55
+ "#{prefix}#{hash_key.to_s.gsub(/ /,'_')}"
56
+ end
57
+
58
+ def keys_to_save
59
+ DEFAULT_KEYS_TO_SAVE
60
+ end
61
+
62
+ def flush_db
63
+ db.flush_db!
64
+ end
65
+
66
+ def update_attribute(id, key, value)
67
+ db.lset!("#{prefix}#{id}", keys_to_save.index(key), Marshal.dump(value))
68
+ end
69
+
70
+ def set(id, value, raw=true)
71
+ db.set!(key(id), raw ? value : Marshal.dump(value))
72
+ end
73
+
74
+ def get(id, raw=true)
75
+ value=db.get(key(id))
76
+ ((raw || !value) ? value : Marshal.load(value))
77
+ end
78
+
79
+ def build_key(model)
80
+ key(model_id(model))
81
+ end
82
+
83
+ def model_id(model)
84
+ model.id
85
+ end
86
+
87
+ def update_attributes(model, attributes)
88
+ id=model_id(model)
89
+ attributes.each do |name|
90
+ update_attribute(id, name, model.send(name))
91
+ end
92
+ end
93
+
94
+ def add_or_update(model)
95
+ id=build_key(model)
96
+ keys_to_save.each do |key|
97
+ db.tail_push!(id, Marshal.dump(model.send(key)))
98
+ end
99
+ db.ltrim!(id, 0, keys_to_save.length)
100
+ end
101
+
102
+ def load_by_range(key, ranges)
103
+ result=[]
104
+ ranges.each do |range|
105
+ db.lrange(key,range.first,range.last).each {|v| result << Marshal.load(v); }
106
+ end
107
+ result
108
+ end
109
+
110
+ def load_values(id, *keys)
111
+ load_by_range(key(id), to_index_range(keys))
112
+ end
113
+
114
+ def to_index_range(keys)
115
+ keys.flatten.map {|key| keys_to_save.index(key.to_sym) }.to_ranges
116
+ end
117
+ private :to_index_range
118
+
119
+ end
120
+
121
+ end
@@ -0,0 +1,213 @@
1
+ #encoding: utf-8
2
+ =begin
3
+ Copyright 2010 Denis Kokorin <virkdi@mail.ru>
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+ =end
17
+
18
+
19
+ require 'thread'
20
+
21
+ module JOffice
22
+ class RedisManager
23
+ include Singleton
24
+ attr_reader :db_prefix
25
+ include JOffice::FiberEvents
26
+ def error_callback
27
+ @error_callback||= lambda {|code|
28
+ #$log.error("Redis"){"[RedisEM]Error code: #{code}"}
29
+ puts "[RedisEM]Error code: #{code}";
30
+ #JOffice::LogOutputter.flush;
31
+ #EM.next_tick{EM.stop}
32
+ }
33
+ end
34
+
35
+ def flush_all
36
+ open_redis.flush_all
37
+ end
38
+
39
+ def increment_key
40
+ :"db.increment"
41
+ end
42
+
43
+ private :increment_key
44
+
45
+ def init_ping_timer
46
+ @@ping_fiber||=Fiber.new{
47
+ while Fiber.yield
48
+ p "[Redis] ping"
49
+ (@@redis_em||={}).values.each do |r|
50
+ r.exists!('0')
51
+ end
52
+ end
53
+ }
54
+ EventMachine::PeriodicTimer.new(10.minutes) do
55
+ @@ping_fiber.resume true
56
+ end
57
+ end
58
+ private :init_ping_timer
59
+
60
+ def start_ping_timer
61
+ @@ping_timer_thread||=init_ping_timer
62
+ end
63
+ private :start_ping_timer
64
+
65
+ def config=(v)
66
+ v.symbolize_keys!
67
+ @config=v
68
+ @db_prefix=(config[:prefix] || 'test')
69
+ p "DB_PREFIX: #{db_prefix}"
70
+ end
71
+
72
+ def load_all_names(redis)
73
+ @db_names={}
74
+ a=redis.keys('db:*')
75
+ a.each do |name|
76
+ @db_names[name]=redis.get(name)
77
+ end if a
78
+ end
79
+ private :load_all_names
80
+
81
+ def config
82
+ @config||={}
83
+ end
84
+
85
+ def logger=(v)
86
+ @logger=v
87
+ end
88
+
89
+ def logger
90
+ @logger||=nil
91
+ end
92
+
93
+ def db_names
94
+ @db_names||={}
95
+ end
96
+
97
+ private :db_names
98
+
99
+ def last_access
100
+ @last_access||={}
101
+ end
102
+ private :last_access
103
+
104
+ #def new_redis(db=1)
105
+ # ::Redis.new({:logger=>logger,:db=>db}.merge(config))
106
+ #end
107
+
108
+ #def get_or_open_redis_instance(db=1, forse=false)
109
+ # key=:"redisdb_#{db_prefix}_#{db}"
110
+ #
111
+ # if forse
112
+ # p "Reopen db #{db}"
113
+ # Thread.current[key]=new_redis(db)
114
+ # else
115
+ # Thread.current[key]||=new_redis(db)
116
+ # end
117
+ #
118
+ #end
119
+ #private :get_or_open_redis_instance
120
+
121
+ def force_open_em_redis(db)
122
+ redis = EM::Protocols::Redis.connect((config[:host] || '127.0.0.1'), (config[:port] || 6379).to_i)
123
+ redis.on_error(&error_callback)
124
+ redis.select(db)
125
+ redis
126
+ end
127
+ private :force_open_em_redis
128
+
129
+ def redis_em
130
+ @@redis_em||=Hash.new { |hash, id| hash[id] = force_open_em_redis(id); };
131
+ end
132
+
133
+ def open_em_redis(db=1)
134
+ redis_em[db]
135
+ end
136
+
137
+ #private :open_em_redis
138
+ def semaphore
139
+ @semaphore||=Mutex.new
140
+ end
141
+
142
+ def register_database(key)
143
+ register_fiber_and_singleton_task("register_database(#{key}) ") do
144
+ redis=open_em_redis
145
+ id=redis.get(key)
146
+ if (new_db=id.nil?)
147
+ id=redis.incr(increment_key)
148
+ while id<2
149
+ id=redis.incr(increment_key)
150
+ end
151
+ redis.set(key,id)
152
+ end
153
+ p "Open db #{key} id=#{id} #{new_db}"
154
+ db_names[key]=id
155
+ end
156
+ end
157
+
158
+ # Lock the object so no other instances can modify it.
159
+ # This method implements the design pattern for locks
160
+ # described at: http://code.google.com/p/redis/wiki/SetnxCommand
161
+ #
162
+ # @see Model#mutex
163
+ def lock!
164
+ db=open_em_redis
165
+ until db.setnx('_lock', lock_timeout)
166
+ next unless lock = db.get('_lock')
167
+ sleep(2) and next unless lock_expired?(lock)
168
+ break unless lock = db.getset('_lock', lock_timeout)
169
+ break if lock_expired?(lock)
170
+ end
171
+ end
172
+
173
+ # Release the lock.
174
+ # @see Model#mutex
175
+ def unlock!
176
+ open_em_redis.del('_lock')
177
+ end
178
+
179
+ def lock_timeout
180
+ Time.now.to_f + 5
181
+ end
182
+
183
+ def lock_expired? lock
184
+ lock.to_f < Time.now.to_f
185
+ end
186
+
187
+ def open_db_em(name, force=false)
188
+ key= :"db:#{db_prefix}_#{name}"
189
+ open_em_redis(db_names[key] || register_database(key))
190
+ end
191
+
192
+ # def opend_db(name, forse=false)
193
+ # redis=nil
194
+ # key= :"db:#{db_prefix}_#{name}"
195
+ # new_db=false
196
+ # id=(db_names[key] || (new_db=register_database(key)))
197
+ #
198
+ # redis=get_or_open_redis_instance(id, forse)
199
+ # redis.flush_db if new_db
200
+ #
201
+ # if block_given?
202
+ # begin
203
+ # yield redis
204
+ # rescue
205
+ # p "Error #{$!}"
206
+ # yield get_or_open_redis_instance(id, true)
207
+ # end
208
+ # end
209
+ # #last_access[id]=Time.new.to_i
210
+ # redis
211
+ # end
212
+ end
213
+ end
@@ -0,0 +1,445 @@
1
+ #encoding: utf-8
2
+ =begin
3
+ Copyright 2010 Denis Kokorin <virkdi@mail.ru>
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+ =end
17
+
18
+
19
+ require 'fileutils'
20
+ require 'em-redis'
21
+ require 'active_support'
22
+
23
+ module EventMachine
24
+ module Protocols
25
+ class RedisMethodFactory
26
+ def initialize(parent)
27
+ @class_name='syncronized_redis_protocol'
28
+ #@buffer=['', "require 'em-redis'","require 'active_support'",'module EventMachine', 'module Protocols', 'module Redis']
29
+ @buffer=['']
30
+ yield self if block_given?
31
+ method 'ping' do |m|
32
+ m << 'exists?(0)'
33
+ end
34
+ method('blank?', 'args') do |m|
35
+ m << "args.blank? || args.flatten!.blank?"
36
+ end
37
+ #@buffer << 'end'
38
+ #@buffer << 'end'
39
+ #@buffer << 'end'
40
+ #generate
41
+ parent.class_eval @buffer.join("\n")
42
+ end
43
+
44
+ def method(name, args=nil)
45
+ buf=[]
46
+ yield buf
47
+ buf=buf.map{|v| " #{v}"}.join("\n")
48
+ signature=(args ? "#{name}(#{args})" : name)
49
+ @buffer << ''
50
+ @buffer << "def #{signature}\n#{buf}\nend"
51
+ @buffer << "public :#{name}"
52
+ @buffer << ''
53
+ end
54
+
55
+ def method_call_args
56
+ @method_call_args||=['', 'key', 'key, value']
57
+ end
58
+
59
+ def method_call(name, options={})
60
+ name_sync="#{name}_sync"
61
+ name_async="#{name}_async"
62
+
63
+ args = (method_call_args[options[:args_count] || 1] || '*args')
64
+
65
+ call_args = case (options[:args_count] || 1)
66
+ when 2 then "[:#{name}, key, value]"
67
+ when 1 then "[:#{name}, key]"
68
+ when 0 then "[:#{name}]"
69
+ else "args.unshift(:#{name})"
70
+ end
71
+
72
+ method(name_sync, args) do |m|
73
+ m << "return [] if blank?(args)" if (options[:args_count] || 1)>3
74
+ m << "c=Fiber.current"
75
+ m << "call_command(#{call_args}) { |a| c.resume(a); }"
76
+ m << "Fiber.yield"
77
+ end
78
+
79
+ method(name_async, args) do |m|
80
+ m << "return if blank?(args)" if (options[:args_count] || 1)>3
81
+ m << "call_command(#{call_args}) { |a| }"
82
+ end
83
+
84
+ @buffer << "alias :#{name}! :#{name_async}"
85
+ @buffer << "alias :#{name} :#{options[:sync] ? name_sync : name_async}"
86
+
87
+ if options[:alias]
88
+ options[:alias].each do |alias_name|
89
+ @buffer << "alias :#{alias_name} :#{name}"
90
+ end
91
+ end
92
+ end
93
+
94
+ def method_alias(name, options={})
95
+ em_name="#{name}_real"
96
+ name_sync="#{name}_sync"
97
+ name_async="#{name}_async"
98
+ @buffer << "alias :#{em_name} :#{name}"
99
+ args = (method_call_args[options[:args_count].to_i || 1] || '*args')
100
+
101
+ method(name_sync, args) do |m|
102
+ m << "return [] if blank?(args)" if (options[:args_count] || 1)>3
103
+ m << "c=Fiber.current"
104
+ m << "#{em_name}(#{args}) { |a| c.resume(a); }"
105
+ m << "Fiber.yield"
106
+ end
107
+
108
+ if options[:cache]
109
+ @buffer << "alias :#{name_sync}_no_cache :#{name_async}"
110
+ method(name_sync, args) do |m|
111
+ m << "@cache_#{name}||=#{name_sync}_no_cache(#{args})"
112
+ end
113
+ end
114
+
115
+ method(name_async, args) do |m|
116
+ m << "return if blank?(args)" if (options[:args_count] || 1)>3
117
+ m << "#{em_name}(#{args}) { |a| }"
118
+ end
119
+
120
+ @buffer << "alias :#{name}! :#{name_async}"
121
+ @buffer << "alias :#{name} :#{options[:sync] ? name_sync : name_async}"
122
+
123
+ if options[:alias]
124
+ options[:alias].each do |alias_name|
125
+ @buffer << "alias :#{alias_name} :#{name}"
126
+ end
127
+ end
128
+ end
129
+
130
+ def generate
131
+ dir=File.join(File.absolute_path(File.dirname(__FILE__)),'metadata')
132
+ FileUtils.mkdir_p(dir)
133
+
134
+ path=File.join(dir, "#{@class_name}.rb")
135
+ f=File.open(path,'w')
136
+ f.write(@buffer.join("\n"))
137
+ f.close
138
+ #require path
139
+ end
140
+ end
141
+
142
+ end
143
+ end
144
+
145
+ module EventMachine
146
+ module Protocols
147
+ module Redis
148
+ RedisMethodFactory.new(self) do |m|
149
+ #m.method_alias :set , {:sync=>false, :args_count=>2}
150
+ #m.method_alias :sort , {:sync=>true}
151
+ m.method_alias :incr , {:sync=>true, :alias=>[:increment], :args_count=>1}
152
+ m.method_alias :decr , {:sync=>false, :alias=>[:decrement], :args_count=>1}
153
+ m.method_alias :mapped_mget , {:sync=>true, :args_count=>999999}
154
+ m.method_alias :quit , {:sync=>false}
155
+ m.method_alias :type , {:sync=>true, :alias=>[:type?], :args_count=>1}
156
+
157
+ m.method_call :keys , {:sync=>true, :args_count=>1}
158
+ m.method_call :get , {:sync=>true}
159
+ m.method_call :getset , {:sync=>true, :args_count=>2}
160
+ #m.method_call :incrby , {:sync=>true, :args_count=>2}
161
+ m.method_call :select , {:sync=>false, :args_count=>1}
162
+ m.method_call :scard , {:sync=>true, :alias=>[:set_count], :args_count=>1}
163
+ m.method_call :smembers , {:sync=>true, :alias=>[:set_member?], :args_count=>1}
164
+ m.method_call :rename , {:sync=>false, :args_count=>2}
165
+ m.method_call :flushall , {:sync=>false, :alias=>[:flush_all], :args_count=>0}
166
+ m.method_call :flushdb , {:sync=>false, :alias=>[:flush_db, :flush_db!], :args_count=>0}
167
+ m.method_call :srem , {:sync=>false, :alias=>[:set_delete, :set_remove, :set_del], :args_count=>2}
168
+ m.method_call :sadd , {:sync=>false, :alias=>[:set_add], :args_count=>2}
169
+
170
+ m.method_call :smembers , {:sync=>true, :alias=>[:members, :set_members], :args_count=>1}
171
+ m.method_call :srandmember , {:sync=>true, :args_count=>1}
172
+
173
+ m.method_call :sismember , {:sync=>true, :alias=>[:set_member?], :args_count=>2}
174
+ m.method_call :exists , {:sync=>true, :alias=>[:exists?], :args_count=>1}
175
+ m.method_call :version , {:sync=>true, :args_count=>0, :cache=>true}
176
+
177
+ m.method_call :hset , {:sync=>true, :args_count=>3}
178
+
179
+ [:multi, :exec, :discard].each do |name|
180
+ m.method_call name, {:sync=>false, :args_count=>0}
181
+ end
182
+
183
+ end
184
+
185
+
186
+ MULTI_BULK_COMMANDS['hset']=true
187
+ MULTI_BULK_COMMANDS['hdel']=true
188
+
189
+ def bulk_array
190
+ @bulk_array||=[]
191
+ end
192
+
193
+ def delete_array
194
+ @delete_array||=[]
195
+ end
196
+
197
+ def bulk_expire_array
198
+ @bulk_expire_array||=[]
199
+ end
200
+
201
+ def bulk_insert?
202
+ !@bulk_insert.nil?
203
+ end
204
+
205
+ =begin
206
+ Добавление всех данных в одной транзакции
207
+ Redis send:
208
+ MULTI
209
+ COMMAND_1 ...
210
+ COMMAND_2 ...
211
+ COMMAND_N ...
212
+ EXEC or DISCARD
213
+ =end
214
+ def transaction
215
+ return unless block_given?
216
+ begin
217
+ multi
218
+ yield
219
+ exec
220
+ rescue Exception => e
221
+ discard
222
+ raise e
223
+ end
224
+ end
225
+
226
+ def bulk_set
227
+ bulk_insert=@bulk_insert.nil?
228
+ begin
229
+ @bulk_insert=true
230
+ yield if block_given?
231
+ if bulk_insert
232
+ @bulk_insert=nil
233
+ del_real(delete_array)
234
+ mset_real(bulk_array)
235
+ bulk_expire_array.each do |value|
236
+ call_command(value) { |a| }
237
+ end
238
+ end
239
+ ensure
240
+ if bulk_insert
241
+ delete_array.clear
242
+ bulk_array.clear
243
+ bulk_expire_array.clear
244
+ end
245
+ end
246
+ end
247
+
248
+ #set
249
+
250
+ def set_async(key, value)
251
+ if bulk_insert?
252
+ bulk_array << key
253
+ bulk_array << value
254
+ else
255
+ call_command(['set', key, value]) { |a| }
256
+ end
257
+ end
258
+
259
+ alias :set! :set_async
260
+ alias :set :set_async
261
+
262
+ def mset_real(args)
263
+ argv=args.flatten
264
+ call_command(argv.unshift('mset')) { |a| } unless argv.empty?
265
+ end
266
+
267
+ def mset_async(*args)
268
+ if bulk_insert?
269
+ bulk_array+=args
270
+ else
271
+ mset_real(args)
272
+ end
273
+ end
274
+
275
+ alias :mset! :mset_async
276
+ alias :mset :mset_async
277
+
278
+ def expire_async(key, value)
279
+ if bulk_insert?
280
+ bulk_expire_array << ['expire', key, value]
281
+ else
282
+ call_command(['expire', key, value]) { |a| }
283
+ end
284
+ end
285
+
286
+ alias :expire! :expire_async
287
+ alias :expire :expire_async
288
+ #end set
289
+
290
+ def del_real(keys)
291
+ call_command(keys.unshift('del')) { |a| } unless keys.empty?
292
+ end
293
+
294
+ def del_async(*agrs)
295
+ values=agrs.flatten
296
+ if bulk_insert?
297
+ delete_array += values
298
+ else
299
+ del_real(values)
300
+ end
301
+ end
302
+
303
+ alias :del! :del_async
304
+ alias :del :del_async
305
+ alias :delete :del_async
306
+
307
+ def mget(keys)
308
+ return [] if keys.blank?
309
+ #keys.flatten!
310
+ #return [] if keys.empty?
311
+ #$log.debug("Redis"){"call mget (#{args.inspect})"} if $debug
312
+
313
+ c=Fiber.current
314
+ call_command(keys.flatten.unshift('mget')) { |a| c.resume(a); }
315
+ Fiber.yield
316
+ end
317
+
318
+ def set_remove_if_empty(key_name, value)
319
+ set_remove(key_name, value)
320
+ del(key_name) if scard(key_name)==0
321
+ end
322
+
323
+ def method_missing(*argv)
324
+ name=argv[0].to_s
325
+ p "call #{name} (#{argv[1..argv.length]})"
326
+ if $debug && false
327
+ p "call #{name} (#{argv[1..argv.length]})"
328
+ #pp Kernel.caller
329
+ #$log.debug("Redis"){"call #{name} (#{argv[1..argv.length]})"}
330
+ argv[0]=(name=name[0,name.length-1]) if name.end_with?('!')
331
+ end
332
+ if name.end_with?('!')
333
+ argv[0]=name[0,name.length-1]
334
+ call_command(argv) {|a| }
335
+ else
336
+ c=Fiber.current
337
+ call_command(argv) { |a| c.resume(a); }
338
+ Fiber.yield
339
+ end
340
+ end
341
+
342
+ def support_mset?
343
+ @support_mset||=version >= "1.1"
344
+ end
345
+
346
+ def raw_call_command(args, &blk)
347
+ argv = args.flatten.map{|v| v.to_s}
348
+
349
+ if MULTI_BULK_COMMANDS[argv.first]
350
+ command = "*#{argv.size}\r\n"
351
+ argv.each do |v|
352
+ command << "$#{get_size(v)}\r\n"
353
+ command << "#{v}\r\n"
354
+ end
355
+ else
356
+ name = argv[0].downcase
357
+ argv[0] = (ALIASES[name] || name)
358
+ raise "#{name} command is disabled" if DISABLED_COMMANDS[name]
359
+ if argv.length > 2 and BULK_COMMANDS[name]
360
+ bulk=argv[-1]
361
+ argv[-1]=get_size(bulk)
362
+ end
363
+ command = "#{argv.join(' ')}\r\n"
364
+ command << "#{bulk}\r\n" if bulk
365
+ end
366
+
367
+ puts "*** sending: #{command}" if $debug
368
+ @redis_callbacks << [REPLY_PROCESSOR[argv[0]], blk]
369
+ send_data command
370
+ end
371
+
372
+
373
+ def process_cmd(line)
374
+ puts "*** processing #{line}" if $debug
375
+ # first character of buffer will always be the response type
376
+ reply_type = line[0, 1]
377
+ reply_args = line.slice(1..-3) # remove type character and \r\n
378
+ case reply_type
379
+
380
+ #e.g. -MISSING
381
+ when MINUS
382
+ @redis_callbacks.shift # throw away the cb?
383
+ if @err_cb
384
+ @err_cb.call(reply_args)
385
+ else
386
+ err = RedisError.new
387
+ err.code = reply_args
388
+ raise err, "Redis server returned error code: #{err.code}"
389
+ end
390
+
391
+ # e.g. +OK
392
+ when PLUS
393
+ dispatch_response(reply_args)
394
+
395
+ # e.g. $3\r\nabc\r\n
396
+ # 'bulk' is more complex because it could be part of multi-bulk
397
+ when DOLLAR
398
+ data_len = Integer(reply_args)
399
+ if data_len == -1 # expect no data; return nil
400
+ if @multibulk_n > 0 # we're in the middle of a multibulk reply
401
+ @values << nil
402
+ if @values.size == @multibulk_n # DING, we're done
403
+ dispatch_response(@values)
404
+ @values = []
405
+ @multibulk_n = 0
406
+ end
407
+ else
408
+ dispatch_response(nil)
409
+ end
410
+ elsif @buffer.size >= data_len + 2 # buffer is full of expected data
411
+ if @multibulk_n > 0 # we're in the middle of a multibulk reply
412
+ @values << @buffer.slice!(0, data_len)
413
+ if @values.size == @multibulk_n # DING, we're done
414
+ dispatch_response(@values)
415
+ @values = []
416
+ @multibulk_n = 0
417
+ end
418
+ else # not multibulk
419
+ value = @buffer.slice!(0, data_len)
420
+ dispatch_response(value)
421
+ end
422
+ @buffer.slice!(0,2) # tossing \r\n
423
+ else # buffer isn't full or nil
424
+ # FYI, ParseError puts command back on head of buffer, waits for
425
+ # more data complete buffer
426
+ raise ParserError
427
+ end
428
+
429
+ #e.g. :8
430
+ when COLON
431
+ dispatch_response(Integer(reply_args))
432
+
433
+ #e.g. *2\r\n$1\r\na\r\n$1\r\nb\r\n
434
+ when ASTERISK
435
+ @multibulk_n = Integer(reply_args)
436
+ dispatch_response(nil) if @multibulk_n == -1 || @multibulk_n == 0
437
+
438
+ # Whu?
439
+ else
440
+ raise ProtocolError, "reply type not recognized: #{line.strip}"
441
+ end
442
+ end
443
+ end
444
+ end
445
+ end