superfeedr-em-redis 0.2.2

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