superfeedr-em-redis 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ .DS_Store
2
+ pkg
data/History.txt ADDED
@@ -0,0 +1,22 @@
1
+ == 0.2.2 / 2009-12-29
2
+ * reselect database after reconnecting
3
+
4
+ == 0.2.1 / 2009-12-15
5
+ * updated gem dependencies
6
+
7
+ == 0.2 / 2009-12-15
8
+ * rip off stock redis gem
9
+ * sort is no longer compatible with 0.1 version
10
+ * response of exists, sismember, sadd, srem, smove, zadd, zrem, move, setnx, del, renamenx, and expire is either true or false, not 0 or 1 as in 0.1
11
+ * info returns hash of symbols now
12
+ * lrem has different argument order
13
+
14
+ == 0.1.1 / 2009-05-01
15
+
16
+ * added a number of aliases to redis-based method names
17
+ * refactored process_cmd method for greater clarity
18
+
19
+ == 0.1.0 / 2009-04-28
20
+
21
+ * initial release
22
+ * compatible with Redis 0.093
data/README.rdoc ADDED
@@ -0,0 +1,85 @@
1
+ == EM-REDIS
2
+
3
+ == DESCRIPTION:
4
+
5
+ An EventMachine[http://rubyeventmachine.com/] based library for interacting with the very cool Redis[http://code.google.com/p/redis/] data store by Salvatore 'antirez' Sanfilippo.
6
+ Modeled after eventmachine's implementation of the memcached protocol, and influenced by Ezra Zygmuntowicz's {redis-rb}[http://github.com/ezmobius/redis-rb/tree/master] library (distributed as part of Redis).
7
+
8
+ This library is only useful when used as part of an application that relies on
9
+ Event Machine's event loop. It implements an EM-based client protocol, which
10
+ leverages the non-blocking nature of the EM interface to achieve significant
11
+ parallelization without threads.
12
+
13
+
14
+ == FEATURES/PROBLEMS:
15
+
16
+ Implements most Redis commands (see {the list of available commands here}[http://code.google.com/p/redis/wiki/CommandReference] with the notable
17
+ exception of MONITOR.
18
+
19
+ == SYNOPSIS:
20
+
21
+ Like any Deferrable eventmachine-based protocol implementation, using EM-Redis involves making calls and passing blocks that serve as callbacks when the call returns.
22
+
23
+ require 'em-redis'
24
+
25
+ EM.run do
26
+ redis = EM::Protocols::Redis.connect
27
+ redis.errback do |code|
28
+ puts "Error code: #{code}"
29
+ end
30
+ redis.set "a", "foo" do |response|
31
+ redis.get "a" do |response|
32
+ puts response
33
+ end
34
+ end
35
+ # We get pipelining for free
36
+ redis.set("b", "bar")
37
+ redis.get("a") do |response|
38
+ puts response # will be foo
39
+ end
40
+ end
41
+
42
+ To run tests on a Redis server (currently compatible with 1.3)
43
+
44
+ rake
45
+
46
+ Because the EM::Protocol::Memcached code used Bacon for testing, test code is
47
+ currently in the form of bacon specs.
48
+
49
+ == REQUIREMENTS:
50
+
51
+ * Redis (download[http://code.google.com/p/redis/downloads/list])
52
+
53
+ == INSTALL:
54
+
55
+ sudo gem install em-redis
56
+
57
+ == LICENSE:
58
+
59
+ (The MIT License)
60
+
61
+ Copyright (c) 2008, 2009
62
+
63
+ Permission is hereby granted, free of charge, to any person obtaining
64
+ a copy of this software and associated documentation files (the
65
+ 'Software'), to deal in the Software without restriction, including
66
+ without limitation the rights to use, copy, modify, merge, publish,
67
+ distribute, sublicense, and/or sell copies of the Software, and to
68
+ permit persons to whom the Software is furnished to do so, subject to
69
+ the following conditions:
70
+
71
+ The above copyright notice and this permission notice shall be
72
+ included in all copies or substantial portions of the Software.
73
+
74
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
75
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
76
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
77
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
78
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
79
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
80
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
81
+
82
+ == CREDIT
83
+
84
+ by Jonathan Broad (http://www.relativepath.org)
85
+
data/Rakefile ADDED
@@ -0,0 +1,41 @@
1
+ # Look in the tasks/setup.rb file for the various options that can be
2
+ # configured in this Rakefile. The .rake files in the tasks directory
3
+ # are where the options are used.
4
+
5
+ begin
6
+ require 'bones'
7
+ rescue LoadError
8
+ abort '### Please install the "bones" gem ###'
9
+ end
10
+
11
+ ensure_in_path 'lib'
12
+ require 'em-redis'
13
+
14
+ task :default => ['redis:test']
15
+
16
+ Bones {
17
+ name 'superfeedr-em-redis'
18
+ authors ['Jonathan Broad', 'Eugene Pimenov']
19
+ email 'libc@me.com'
20
+ url 'http://github.com/libc/em-redis'
21
+ summary 'An eventmachine-based implementation of the Redis protocol'
22
+ description summary
23
+ version EMRedis::VERSION
24
+
25
+ readme_file 'README.rdoc'
26
+ ignore_file '.gitignore'
27
+
28
+ depend_on 'eventmachine', '>=0.12.10'
29
+
30
+ depend_on "bacon", :development => true
31
+ depend_on "em-spec", :development => true
32
+ }
33
+
34
+ namespace :redis do
35
+ desc "Test em-redis against a live Redis"
36
+ task :test do
37
+ sh "bacon spec/live_redis_protocol_spec.rb spec/redis_commands_spec.rb spec/redis_protocol_spec.rb"
38
+ end
39
+ end
40
+
41
+ # EOF
@@ -0,0 +1,446 @@
1
+ require 'rubygems'
2
+ require 'eventmachine'
3
+
4
+ module EventMachine
5
+ module Protocols
6
+ module Redis
7
+ include EM::Deferrable
8
+
9
+ ##
10
+ # constants
11
+ #########################
12
+
13
+ OK = "OK".freeze
14
+ MINUS = "-".freeze
15
+ PLUS = "+".freeze
16
+ COLON = ":".freeze
17
+ DOLLAR = "$".freeze
18
+ ASTERISK = "*".freeze
19
+ DELIM = "\r\n".freeze
20
+
21
+ BULK_COMMANDS = {
22
+ "set" => true,
23
+ "setnx" => true,
24
+ "rpush" => true,
25
+ "lpush" => true,
26
+ "lset" => true,
27
+ "lrem" => true,
28
+ "sadd" => true,
29
+ "srem" => true,
30
+ "sismember" => true,
31
+ "echo" => true,
32
+ "getset" => true,
33
+ "smove" => true,
34
+ "zadd" => true,
35
+ "zincrby" => true,
36
+ "zrem" => true,
37
+ "zscore" => true
38
+ }
39
+
40
+ MULTI_BULK_COMMANDS = {
41
+ "mset" => true,
42
+ "msetnx" => true,
43
+ # these aliases aren't in redis gem
44
+ "multi_get" => true
45
+ }
46
+
47
+ BOOLEAN_PROCESSOR = lambda{|r| r == 1 }
48
+
49
+ REPLY_PROCESSOR = {
50
+ "exists" => BOOLEAN_PROCESSOR,
51
+ "sismember" => BOOLEAN_PROCESSOR,
52
+ "sadd" => BOOLEAN_PROCESSOR,
53
+ "srem" => BOOLEAN_PROCESSOR,
54
+ "smove" => BOOLEAN_PROCESSOR,
55
+ "zadd" => BOOLEAN_PROCESSOR,
56
+ "zrem" => BOOLEAN_PROCESSOR,
57
+ "move" => BOOLEAN_PROCESSOR,
58
+ "setnx" => BOOLEAN_PROCESSOR,
59
+ "del" => BOOLEAN_PROCESSOR,
60
+ "renamenx" => BOOLEAN_PROCESSOR,
61
+ "expire" => BOOLEAN_PROCESSOR,
62
+ "select" => BOOLEAN_PROCESSOR, # not in redis gem
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
+ # these aliases aren't in redis gem
117
+ "background_save" => 'bgsave',
118
+ "async_save" => 'bgsave',
119
+ "members" => 'smembers',
120
+ "decrement_by" => "decrby",
121
+ "decrement" => "decr",
122
+ "increment_by" => "incrby",
123
+ "increment" => "incr",
124
+ "set_if_nil" => "setnx",
125
+ "multi_get" => "mget",
126
+ "random_key" => "randomkey",
127
+ "random" => "randomkey",
128
+ "rename_if_nil" => "renamenx",
129
+ "tail_pop" => "rpop",
130
+ "pop" => "rpop",
131
+ "head_pop" => "lpop",
132
+ "shift" => "lpop",
133
+ "list_remove" => "lrem",
134
+ "index" => "lindex",
135
+ "trim" => "ltrim",
136
+ "list_range" => "lrange",
137
+ "range" => "lrange",
138
+ "list_len" => "llen",
139
+ "len" => "llen",
140
+ "head_push" => "lpush",
141
+ "unshift" => "lpush",
142
+ "tail_push" => "rpush",
143
+ "push" => "rpush",
144
+ "add" => "sadd",
145
+ "set_remove" => "srem",
146
+ "set_size" => "scard",
147
+ "member?" => "sismember",
148
+ "intersect" => "sinter",
149
+ "intersect_and_store" => "sinterstore",
150
+ "members" => "smembers",
151
+ "exists?" => "exists"
152
+ }
153
+
154
+ DISABLED_COMMANDS = {
155
+ "monitor" => true,
156
+ "sync" => true
157
+ }
158
+
159
+ def []=(key,value)
160
+ set(key,value)
161
+ end
162
+
163
+ def set(key, value, expiry=nil)
164
+ call_command([:set, key, value]) do |s|
165
+ yield s if block_given?
166
+ end
167
+ expire(key, expiry) if expiry
168
+ end
169
+
170
+ def sort(key, options={}, &blk)
171
+ cmd = ["SORT"]
172
+ cmd << key
173
+ cmd << "BY #{options[:by]}" if options[:by]
174
+ cmd << "GET #{[options[:get]].flatten * ' GET '}" if options[:get]
175
+ cmd << "#{options[:order]}" if options[:order]
176
+ cmd << "LIMIT #{options[:limit].join(' ')}" if options[:limit]
177
+ call_command(cmd, &blk)
178
+ end
179
+
180
+ def incr(key, increment = nil, &blk)
181
+ call_command(increment ? ["incrby",key,increment] : ["incr",key], &blk)
182
+ end
183
+
184
+ def decr(key, decrement = nil, &blk)
185
+ call_command(decrement ? ["decrby",key,decrement] : ["decr",key], &blk)
186
+ end
187
+
188
+ def select(db, &blk)
189
+ @db = db.to_i
190
+ call_command(['select', @db], &blk)
191
+ end
192
+
193
+ def auth(password, &blk)
194
+ @password = password
195
+ call_command(['auth', password], &blk)
196
+ end
197
+
198
+ # Similar to memcache.rb's #get_multi, returns a hash mapping
199
+ # keys to values.
200
+ def mapped_mget(*keys)
201
+ mget(*keys) do |response|
202
+ result = {}
203
+ response.each do |value|
204
+ key = keys.shift
205
+ result.merge!(key => value) unless value.nil?
206
+ end
207
+ yield result if block_given?
208
+ end
209
+ end
210
+
211
+ # Ruby defines a now deprecated type method so we need to override it here
212
+ # since it will never hit method_missing
213
+ def type(key, &blk)
214
+ call_command(['type', key], &blk)
215
+ end
216
+
217
+ def quit(&blk)
218
+ call_command(['quit'], &blk)
219
+ end
220
+
221
+ def errback(&blk)
222
+ @error_callback = blk
223
+ end
224
+ alias_method :on_error, :errback
225
+
226
+ def method_missing(*argv, &blk)
227
+ call_command(argv, &blk)
228
+ end
229
+
230
+ def call_command(argv, &blk)
231
+ callback { raw_call_command(argv, &blk) }
232
+ end
233
+
234
+ def raw_call_command(argv, &blk)
235
+ argv = argv.dup
236
+
237
+ if MULTI_BULK_COMMANDS[argv.flatten[0].to_s]
238
+ # TODO improve this code
239
+ argvp = argv.flatten
240
+ values = argvp.pop.to_a.flatten
241
+ argvp = values.unshift(argvp[0])
242
+ command = ["*#{argvp.size}"]
243
+ argvp.each do |v|
244
+ v = v.to_s
245
+ command << "$#{get_size(v)}"
246
+ command << v
247
+ end
248
+ command = command.map {|cmd| "#{cmd}\r\n"}.join
249
+ else
250
+ command = ""
251
+ bulk = nil
252
+ argv[0] = argv[0].to_s.downcase
253
+ argv[0] = ALIASES[argv[0]] if ALIASES[argv[0]]
254
+ raise "#{argv[0]} command is disabled" if DISABLED_COMMANDS[argv[0]]
255
+ if BULK_COMMANDS[argv[0]] and argv.length > 1
256
+ bulk = argv[-1].to_s
257
+ argv[-1] = get_size(bulk)
258
+ end
259
+ command << "#{argv.join(' ')}\r\n"
260
+ command << "#{bulk}\r\n" if bulk
261
+ end
262
+
263
+ @logger.debug { "*** sending: #{command}" } if @logger
264
+ @redis_callbacks << [REPLY_PROCESSOR[argv[0]], blk]
265
+ send_data command
266
+ end
267
+
268
+ ##
269
+ # errors
270
+ #########################
271
+
272
+ class ParserError < StandardError; end
273
+ class ProtocolError < StandardError; end
274
+
275
+ class RedisError < StandardError
276
+ attr_accessor :code
277
+ end
278
+
279
+
280
+ ##
281
+ # em hooks
282
+ #########################
283
+
284
+ def self.connect(*args)
285
+ case args.length
286
+ when 0
287
+ options = {}
288
+ when 1
289
+ arg = args.shift
290
+ case arg
291
+ when Hash then options = arg
292
+ when String then options = {:host => arg}
293
+ else raise ArgumentError, 'first argument must be Hash or String'
294
+ end
295
+ when 2
296
+ options = {:host => args[1], :port => args[2]}
297
+ else
298
+ raise ArgumentError, "wrong number of arguments (#{args.length} for 1)"
299
+ end
300
+ options[:host] ||= '127.0.0.1'
301
+ options[:port] = (options[:port] || 6379).to_i
302
+ EM.connect options[:host], options[:port], self, options
303
+ end
304
+
305
+ def initialize(options = {})
306
+ @host = options[:host]
307
+ @port = options[:port]
308
+ @db = (options[:db] || 0).to_i
309
+ @password = options[:password]
310
+ @logger = options[:logger]
311
+ @error_callback = lambda do |code|
312
+ err = RedisError.new
313
+ err.code = code
314
+ raise err, "Redis server returned error code: #{code}"
315
+ end
316
+
317
+ # These commands should be first
318
+ auth_and_select_db
319
+ end
320
+
321
+ def auth_and_select_db
322
+ call_command(["auth", @password]) if @password
323
+ call_command(["select", @db]) unless @db == 0
324
+ end
325
+ private :auth_and_select_db
326
+
327
+ def connection_completed
328
+ @logger.debug { "Connected to #{@host}:#{@port}" } if @logger
329
+
330
+ @redis_callbacks = []
331
+ @multibulk_n = false
332
+ @reconnecting = false
333
+ @connected = true
334
+
335
+ succeed
336
+ end
337
+
338
+ # 19Feb09 Switched to a custom parser, LineText2 is recursive and can cause
339
+ # stack overflows when there is too much data.
340
+ # include EM::P::LineText2
341
+ def receive_data(data)
342
+ (@buffer ||= '') << data
343
+ while index = @buffer.index(DELIM)
344
+ begin
345
+ line = @buffer.slice!(0, index+2)
346
+ process_cmd line
347
+ rescue ParserError
348
+ @buffer[0...0] = line
349
+ break
350
+ end
351
+ end
352
+ end
353
+
354
+ def process_cmd(line)
355
+ @logger.debug { "*** processing #{line}" } if @logger
356
+ # first character of buffer will always be the response type
357
+ reply_type = line[0, 1]
358
+ reply_args = line.slice(1..-3) # remove type character and \r\n
359
+ case reply_type
360
+
361
+ #e.g. -MISSING
362
+ when MINUS
363
+ @redis_callbacks.shift # throw away the cb?
364
+ @error_callback.call(reply_args)
365
+ # e.g. +OK
366
+ when PLUS
367
+ dispatch_response(reply_args)
368
+ # e.g. $3\r\nabc\r\n
369
+ # 'bulk' is more complex because it could be part of multi-bulk
370
+ when DOLLAR
371
+ data_len = Integer(reply_args)
372
+ if data_len == -1 # expect no data; return nil
373
+ dispatch_response(nil)
374
+ elsif @buffer.size >= data_len + 2 # buffer is full of expected data
375
+ dispatch_response(@buffer.slice!(0, data_len))
376
+ @buffer.slice!(0,2) # tossing \r\n
377
+ else # buffer isn't full or nil
378
+ # TODO: don't control execution with exceptions
379
+ raise ParserError
380
+ end
381
+ #e.g. :8
382
+ when COLON
383
+ dispatch_response(Integer(reply_args))
384
+ #e.g. *2\r\n$1\r\na\r\n$1\r\nb\r\n
385
+ when ASTERISK
386
+ multibulk_count = Integer(reply_args)
387
+ if multibulk_count == -1
388
+ dispatch_response([])
389
+ else
390
+ start_multibulk(multibulk_count)
391
+ end
392
+ # Whu?
393
+ else
394
+ # TODO: get rid of this exception
395
+ raise ProtocolError, "reply type not recognized: #{line.strip}"
396
+ end
397
+ end
398
+
399
+ def dispatch_response(value)
400
+ if @multibulk_n
401
+ @multibulk_values << value
402
+ @multibulk_n -= 1
403
+
404
+ if @multibulk_n == 0
405
+ value = @multibulk_values
406
+ @multibulk_n = false
407
+ else
408
+ return
409
+ end
410
+ end
411
+
412
+ processor, blk = @redis_callbacks.shift
413
+ value = processor.call(value) if processor
414
+ blk.call(value) if blk
415
+ end
416
+
417
+ def start_multibulk(multibulk_count)
418
+ @multibulk_n = multibulk_count
419
+ @multibulk_values = []
420
+ end
421
+
422
+ def unbind
423
+ @logger.debug { "Disconnected" } if @logger
424
+ if @connected || @reconnecting
425
+ EM.add_timer(1) do
426
+ @logger.debug { "Reconnecting to #{@host}:#{@port}" } if @logger
427
+ reconnect @host, @port
428
+ auth_and_select_db
429
+ end
430
+ @connected = false
431
+ @reconnecting = true
432
+ @deferred_status = nil
433
+ else
434
+ # TODO: get rid of this exception
435
+ raise 'Unable to connect to redis server'
436
+ end
437
+ end
438
+
439
+ private
440
+ def get_size(string)
441
+ string.respond_to?(:bytesize) ? string.bytesize : string.size
442
+ end
443
+
444
+ end
445
+ end
446
+ end