joffice_redis 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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