em-redis 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,11 @@
1
+ == 0.1.1 / 2009-05-01
2
+
3
+ * added a number of aliases to redis-based method names
4
+ * refactored process_cmd method for greater clarity
5
+
6
+ == 0.1.0 / 2009-04-28
7
+
8
+ * initial release
9
+ * compatible with Redis 0.093
10
+
11
+
data/Manifest.txt ADDED
@@ -0,0 +1,10 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.rdoc
4
+ em-redis.gemspec
5
+ Rakefile
6
+ lib/em-redis.rb
7
+ lib/em-redis/redis_protocol.rb
8
+ spec/test_helper.rb
9
+ spec/live_redis_protocol_spec.rb
10
+ spec/redis_protocol_spec.rb
data/README.rdoc ADDED
@@ -0,0 +1,94 @@
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 acheive significant
11
+ parallelization without threads.
12
+
13
+ WARNING: this library is my first attempt to write an evented client protocol,
14
+ and isn't currently used in production anywhere. All that bit in the license
15
+ about not being warranted to work for any particular purpose really applies.
16
+
17
+
18
+ == FEATURES/PROBLEMS:
19
+
20
+ * Implements most Redis commands (see {the list of available commands here}[http://code.google.com/p/redis/wiki/CommandReference] with the notable
21
+ exception of MONITOR and AUTH
22
+
23
+ == TODO:
24
+
25
+ * Right now method names are identical to Redis command names. I'll add some nice aliases soon.
26
+ * I'm sure multibulk responses can be handled more elegantly
27
+ * Better default error handling? Provide a default response block?
28
+
29
+ == SYNOPSIS:
30
+
31
+ * Like any Deferrable eventmachine-based protocol implementation, using EM-Redis involves making calls and passing blocks that serve as callbacks when
32
+ the call returns.
33
+
34
+ require 'em-redis'
35
+
36
+ EM.run do
37
+ redis = EM::Protocol::Redis.connect
38
+ error_callback = lambda {|code| puts "Error code: #{code}" }
39
+ redis.on_error error_callback
40
+ redis.set "a", "foo" do |response|
41
+ redis.get "a" do |response|
42
+ puts response
43
+ end
44
+ end
45
+ end
46
+
47
+ * To run live tests on a Redis server (currently compatible with 0.91)
48
+
49
+ rake redis:live_test
50
+
51
+ * To run a test of the underlying protocol implemention
52
+
53
+ rake redis:offline_test
54
+
55
+ Because the EM::Protocol::Memcached code used Bacon for testing, test code is
56
+ currently in the form of bacon specs. I'll port them to RSpec at some point.
57
+
58
+ == REQUIREMENTS:
59
+
60
+ * Redis (download[http://code.google.com/p/redis/downloads/list])
61
+
62
+ == INSTALL:
63
+
64
+ * sudo gem install madsimian-em-redis --source http://gems.github.com
65
+
66
+ == LICENSE:
67
+
68
+ (The MIT License)
69
+
70
+ Copyright (c) 2008
71
+
72
+ Permission is hereby granted, free of charge, to any person obtaining
73
+ a copy of this software and associated documentation files (the
74
+ 'Software'), to deal in the Software without restriction, including
75
+ without limitation the rights to use, copy, modify, merge, publish,
76
+ distribute, sublicense, and/or sell copies of the Software, and to
77
+ permit persons to whom the Software is furnished to do so, subject to
78
+ the following conditions:
79
+
80
+ The above copyright notice and this permission notice shall be
81
+ included in all copies or substantial portions of the Software.
82
+
83
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
84
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
85
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
86
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
87
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
88
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
89
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
90
+
91
+ == CREDIT
92
+
93
+ by Jonathan Broad (http://www.relativepath.org)
94
+
data/Rakefile ADDED
@@ -0,0 +1,26 @@
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
+ Bones.setup
8
+ rescue LoadError
9
+ load 'tasks/setup.rb'
10
+ end
11
+
12
+ ensure_in_path 'lib'
13
+ require 'em-redis'
14
+
15
+ task :default => 'spec:run'
16
+
17
+ PROJ.name = 'em-redis'
18
+ PROJ.authors = 'Jonathan Broad'
19
+ PROJ.email = 'jonathan@relativepath.org'
20
+ PROJ.url = ''
21
+ PROJ.version = EMRedis::VERSION
22
+ PROJ.rubyforge.name = 'em-redis'
23
+ PROJ.spec.opts << '--color'
24
+ PROJ.gem.dependencies << "bacon"
25
+
26
+ # EOF
data/em-redis.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = %q{em-redis}
3
+ s.version = "0.1.1"
4
+
5
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
6
+ s.authors = ["Jonathan Broad"]
7
+ s.date = %q{2009-04-28}
8
+ s.description = %q{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. 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). This library is only useful when used as part of an application that relies on Event Machine's event loop. It implements an EM-based client protocol, which leverages the non-blocking nature of the EM interface to acheive significant parallelization without threads. WARNING: this library is my first attempt to write an evented client protocol, and isn't currently used in production anywhere. All that bit in the license about not being warranted to work for any particular purpose really applies.}
9
+ s.email = %q{jonathan@relativepath.org}
10
+ s.extra_rdoc_files = ["History.txt", "README.rdoc"]
11
+ s.files = ["History.txt", "Manifest.txt", "README.rdoc", "em-redis.gemspec", "Rakefile", "lib/em-redis.rb", "lib/em-redis/redis_protocol.rb", "spec/test_helper.rb", "spec/live_redis_protocol_spec.rb", "spec/redis_protocol_spec.rb"]
12
+ s.has_rdoc = true
13
+ s.rdoc_options = ["--main", "README.rdoc"]
14
+ s.require_paths = ["lib"]
15
+ s.rubyforge_project = %q{em-redis}
16
+ s.rubygems_version = %q{1.3.0}
17
+ s.summary = %q{An EventMachine[http://rubyeventmachine}
18
+
19
+ if s.respond_to? :specification_version then
20
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
21
+ s.specification_version = 2
22
+
23
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
24
+ s.add_development_dependency(%q<bacon>, [">= 0"])
25
+ s.add_development_dependency(%q<bones>, [">= 2.1.1"])
26
+ else
27
+ s.add_dependency(%q<bacon>, [">= 0"])
28
+ s.add_dependency(%q<bones>, [">= 2.1.1"])
29
+ end
30
+ else
31
+ s.add_dependency(%q<bacon>, [">= 0"])
32
+ s.add_dependency(%q<bones>, [">= 2.1.1"])
33
+ end
34
+ end
35
+
data/lib/em-redis.rb ADDED
@@ -0,0 +1,36 @@
1
+
2
+ module EMRedis
3
+
4
+ # :stopdoc:
5
+ VERSION = '0.1.1'
6
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
7
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
8
+ # :startdoc:
9
+
10
+ # Returns the version string for the library.
11
+ #
12
+ def self.version
13
+ VERSION
14
+ end
15
+
16
+ # Returns the library path for the module. If any arguments are given,
17
+ # they will be joined to the end of the libray path using
18
+ # <tt>File.join</tt>.
19
+ #
20
+ def self.libpath( *args )
21
+ args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
22
+ end
23
+
24
+ # Returns the lpath for the module. If any arguments are given,
25
+ # they will be joined to the end of the path using
26
+ # <tt>File.join</tt>.
27
+ #
28
+ def self.path( *args )
29
+ args.empty? ? PATH : ::File.join(PATH, args.flatten)
30
+ end
31
+
32
+ end # module EMRedis
33
+
34
+ require File.join(EMRedis.libpath, "em-redis/redis_protocol")
35
+
36
+ # EOF
@@ -0,0 +1,479 @@
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
+ unless defined? C_ERR
14
+ C_ERR = "-".freeze
15
+ C_OK = 'OK'.freeze
16
+ C_SINGLE = '+'.freeze
17
+ C_BULK = '$'.freeze
18
+ C_MULTI = '*'.freeze
19
+ C_INT = ':'.freeze
20
+ C_DELIM = "\r\n".freeze
21
+ end
22
+
23
+ #QUIT, AUTH (NOT IMPLEMENTED)
24
+ def quit(&blk)
25
+ inline_command "QUIT", &blk
26
+ end
27
+
28
+ #GET,SET,MGET,SETNX,INCR,INCRBY,DECR,DECRBY,EXISTS,DEL,TYPE
29
+ def type(key, &blk)
30
+ inline_command "TYPE", key, &blk
31
+ end
32
+
33
+ def del(key, &blk) #delete
34
+ inline_command "DEL", key, &blk
35
+ end
36
+ alias_method :delete, :del
37
+
38
+ def exists(key, &blk) #exists?
39
+ inline_command "EXISTS", key, &blk
40
+ end
41
+ alias_method :exists?, :exists
42
+
43
+ def decrby(key, value, &blk) #decrement_by
44
+ inline_command "DECRBY", key, value, &blk
45
+ end
46
+ alias_method :decrement_by, :decrby
47
+
48
+ def decr(key, &blk) #decrement
49
+ inline_command "DECR", key, &blk
50
+ end
51
+ alias_method :decrement, :decr
52
+
53
+ def incrby(key, value, &blk)
54
+ inline_command "INCRBY", key, value, &blk
55
+ end
56
+ alias_method :increment_by, :incrby
57
+
58
+ def incr(key, &blk) #increment
59
+ inline_command "INCR", key, &blk
60
+ end
61
+ alias_method :increment, :incr
62
+
63
+ def setnx(key, value, &blk) #set_if_nil
64
+ multiline_command "SETNX", key, value, &blk
65
+ end
66
+ alias_method :set_if_nil, :setnx
67
+
68
+ def mget(*keys, &blk) #multi_get
69
+ inline_command "MGET", *keys, &blk
70
+ end
71
+ alias_method :multi_get, :mget
72
+
73
+ def set(key, value, &blk)
74
+ multiline_command "SET", key, value, &blk
75
+ end
76
+
77
+ def get(key, &blk)
78
+ inline_command "GET", key, &blk
79
+ end
80
+
81
+ #KEYS,RANDOMKEY,RENAME,RENAMENX,DBSIZE,EXPIRE
82
+ def keys(key_search_string, &blk)
83
+ wrapper = lambda {|v| blk.call(v.split)} # spec suggests splitting response on whitespace
84
+ inline_command "KEYS", key_search_string, &wrapper
85
+ end
86
+
87
+ def randomkey(&blk) #random_key, random
88
+ inline_command "RANDOMKEY", &blk
89
+ end
90
+ alias_method :random_key, :randomkey
91
+ alias_method :random, :randomkey
92
+
93
+ def rename(old_name, new_name, &blk)
94
+ inline_command "RENAME", old_name, new_name, &blk
95
+ end
96
+
97
+ def renamenx(old_name, new_name, &blk) #rename_if_nil
98
+ inline_command "RENAMENX", old_name, new_name, &blk
99
+ end
100
+ alias_method :rename_if_nil, :renamenx
101
+
102
+ def dbsize(&blk)
103
+ inline_command "DBSIZE", &blk
104
+ end
105
+
106
+ def expire(key, time, &blk)
107
+ inline_command "EXPIRE", key, time, &blk
108
+ end
109
+
110
+ #RPUSH,LPUSH,LLEN,LRANGE,LTRIM,LINDEX,LSET,LREM,LPOP,RPOP
111
+ def rpop(key, &blk) # tail_pop, pop
112
+ inline_command "RPOP", key, &blk
113
+ end
114
+ alias_method :tail_pop, :rpop
115
+ alias_method :pop, :rpop
116
+
117
+ def lpop(key, &blk) #head_pop, shift
118
+ inline_command "LPOP", key, &blk
119
+ end
120
+ alias_method :head_pop, :lpop
121
+ alias_method :shift, :lpop
122
+
123
+
124
+ def lrem(key, value, count=0, &blk) #list_remove
125
+ multiline_command "LREM", key, count, value, &blk
126
+ end
127
+ alias_method :list_remove, :lrem
128
+
129
+ def lset(key, index, value, &blk) #list_set
130
+ multiline_command "LSET", key, index, value, &blk
131
+ end
132
+ alias_method :list_set, :lset
133
+
134
+ def lindex(key, index, &blk) #list_index, index
135
+ inline_command "LINDEX", key, index, &blk
136
+ end
137
+ alias_method :list_index, :lindex
138
+ alias_method :index, :lindex
139
+
140
+ def ltrim(key, start, ending, &blk) # list_trim, trim
141
+ inline_command "LTRIM", key, start, ending, &blk
142
+ end
143
+ alias_method :list_trim, :ltrim
144
+ alias_method :trim, :ltrim
145
+
146
+ def lrange(key, start, range, &blk) #list_range, range
147
+ inline_command "LRANGE", key, start, range, &blk
148
+ end
149
+ alias_method :list_range, :lrange
150
+ alias_method :range, :lrange
151
+
152
+ def llen(key, &blk) #list_len, len
153
+ inline_command "LLEN", key, &blk
154
+ end
155
+ alias_method :list_len, :llen
156
+ alias_method :len, :llen
157
+
158
+ def lpush(key, value, &blk) #head_push, unshift
159
+ multiline_command "LPUSH", key, value, &blk
160
+ end
161
+ alias_method :head_push, :lpush
162
+ alias_method :unshift, :lpush
163
+
164
+ def rpush(key, value, &blk) #tail_push, push
165
+ multiline_command "RPUSH", key, value, &blk
166
+ end
167
+ alias_method :tail_push, :rpush
168
+ alias_method :push, :rpush
169
+
170
+
171
+ #SADD,SREM,SCARD,SISMEMBER,SINTER,SINTERSTORE,SUNION,SUNIONSTORE,SMEMBERS
172
+ def sadd(key, value, &blk) #set_add, add
173
+ multiline_command "SADD", key, value, &blk
174
+ end
175
+ alias_method :set_add, :sadd
176
+ alias_method :add, :sadd
177
+
178
+ def srem(key, value, &blk) #set_remove
179
+ multiline_command "SREM", key, value, &blk
180
+ end
181
+ alias_method :set_remove, :srem
182
+
183
+ def scard(key, &blk) # set_size
184
+ inline_command "SCARD", key, &blk
185
+ end
186
+ alias_method :set_size, :scard
187
+
188
+ def sismember(key, value, &blk) #set_member? member?
189
+ multiline_command "SISMEMBER", key, value, &blk
190
+ end
191
+ alias_method :set_member?, :sismember
192
+ alias_method :member?, :sismember
193
+
194
+ def sinter(*keys, &blk) #intersect
195
+ inline_command "SINTER", *keys, &blk
196
+ end
197
+ alias_method :intersect, :sinter
198
+
199
+ def sinterstore(target_key, *keys, &blk) #intersect_and_store
200
+ inline_command "SINTERSTORE", target_key, *keys, &blk
201
+ end
202
+ alias_method :intersect_and_store, :sinterstore
203
+
204
+ # UNION SET MANIP NOT IN RELEASE BUILDS YET
205
+ ############################################
206
+ #
207
+ # def sunion(*keys, &blk)
208
+ # inline_command "SUNION", *keys, &blk
209
+ # end
210
+ #
211
+ # def sunionstore(target_key, *keys, &blk)
212
+ # inline_command "SUNIONSTORE", target_key, *keys, &blk
213
+ # end
214
+ #
215
+ ############################################
216
+
217
+ def smembers(key, &blk) #set_members, members
218
+ inline_command "SMEMBERS", key, &blk
219
+ end
220
+ alias_method :set_members, :smembers
221
+ alias_method :members, :smembers
222
+
223
+
224
+ #SELECT,MOVE,FLUSHDB,FLUSHALL
225
+ def select(db_index, &blk)
226
+ inline_command "SELECT", db_index, &blk
227
+ end
228
+
229
+ def move(key, dbindex, &blk)
230
+ inline_command "MOVE", key, dbindex, &blk
231
+ end
232
+
233
+ def flushdb(&blk)
234
+ inline_command "FLUSHDB", &blk
235
+ end
236
+
237
+ def flushall(&blk)
238
+ inline_command "FLUSHALL", &blk
239
+ end
240
+
241
+ #SORT
242
+ def sort(key, by_pattern=nil, start=nil, ending=nil, get_pattern=nil, desc=false, alpha=false, &blk)
243
+ command = "SORT #{key}"
244
+ command += " BY #{by_pattern}" if by_pattern
245
+ command += " LIMIT #{start} #{ending}" if (start && ending)
246
+ command += " GET #{get_pattern}" if get_pattern
247
+ command += " DESC" if desc
248
+ command += " ALPHA" if alpha
249
+ inline_command command, &blk
250
+ end
251
+
252
+ #SAVE,BGSAVE,LASTSAVE,SHUTDOWN
253
+ def shutdown(&blk)
254
+ inline_command "SHUTDOWN", &blk
255
+ end
256
+
257
+ def lastsave(&blk)
258
+ inline_command "LASTSAVE", &blk
259
+ end
260
+
261
+ def bgsave(&blk) #background_save, async_save
262
+ inline_command "BGSAVE", &blk
263
+ end
264
+ alias_method :background_save, :bgsave
265
+ alias_method :async_save, :bgsave
266
+
267
+ def save(&blk)
268
+ inline_command "SAVE", &blk
269
+ end
270
+
271
+ #INFO,MONITOR
272
+ def info(&blk)
273
+ wrapper = lambda do |r|
274
+ blk.call(
275
+ r.split.inject({}) {|hash,string| key,value = string.split(":"); hash[key] = value; hash }
276
+ )
277
+ end
278
+ inline_command "INFO", &wrapper
279
+ end
280
+
281
+ # MONITOR's a bit tricky
282
+ def monitor
283
+ end
284
+
285
+ def on_error(&blk)
286
+ @err_cb = blk
287
+ end
288
+
289
+
290
+ ##
291
+ # Generic request methods
292
+ #########################
293
+
294
+ def inline_command(*args, &blk)
295
+ callback {
296
+ command = args.shift
297
+ blk ||= lambda { } # all cmds must at least have a no-op callback
298
+ @redis_callbacks << blk
299
+ if args.size > 0
300
+ command += " "
301
+ command += args.join(" ")
302
+ end
303
+ command += C_DELIM
304
+ puts "*** sending: #{command}" if $debug
305
+ send_data command
306
+ }
307
+ end
308
+
309
+ def multiline_command(command, *args, &blk)
310
+ callback {
311
+ data_value = args.pop.to_s
312
+ blk ||= lambda { } # all cmds must at least have a no-op callback
313
+ @redis_callbacks << blk
314
+ command += " "
315
+ if args.size > 0
316
+ command += args.join(" ")
317
+ command += " "
318
+ end
319
+ command += data_value.size.to_s
320
+ command += C_DELIM
321
+ command += data_value
322
+ command += C_DELIM
323
+ puts "*** sending: #{command}" if $debug
324
+ send_data command
325
+ }
326
+ end
327
+
328
+
329
+ ##
330
+ # errors
331
+ #########################
332
+
333
+ class ParserError < StandardError; end
334
+ class ProtocolError < StandardError; end
335
+
336
+ class RedisError < StandardError
337
+ attr_accessor :code
338
+ end
339
+
340
+
341
+ ##
342
+ # em hooks
343
+ #########################
344
+
345
+ def self.connect host = 'localhost', port = 6379
346
+ puts "*** connecting" if $debug
347
+ EM.connect host, port, self, host, port
348
+ end
349
+
350
+ def initialize host, port = 6379
351
+ puts "*** initializing" if $debug
352
+ @host, @port = host, port
353
+ end
354
+
355
+ def connection_completed
356
+ puts "*** connection_complete!" if $debug
357
+ @redis_callbacks = []
358
+ @values = []
359
+ @multibulk_n = 0
360
+
361
+ @reconnecting = false
362
+ @connected = true
363
+ succeed
364
+ end
365
+
366
+ # 19Feb09 Switched to a custom parser, LineText2 is recursive and can cause
367
+ # stack overflows when there is too much data.
368
+ # include EM::P::LineText2
369
+ def receive_data data
370
+ (@buffer||='') << data
371
+ while index = @buffer.index(C_DELIM)
372
+ begin
373
+ line = @buffer.slice!(0,index+2)
374
+ process_cmd line
375
+ rescue ParserError
376
+ @buffer[0...0] = line
377
+ break
378
+ end
379
+ end
380
+ end
381
+
382
+ def process_cmd line
383
+ puts "*** processing #{line}" if $debug
384
+ # first character of buffer will always be the response type
385
+ reply_type = line[0].chr
386
+ reply_args = line.slice(1..-3) # remove type character and \r\n
387
+ case reply_type
388
+
389
+ # e.g. +OK
390
+ when C_SINGLE
391
+ if cb = @redis_callbacks.shift
392
+ cb.call( reply_args )
393
+ end
394
+
395
+ # e.g. $3\r\nabc\r\n
396
+ # 'bulk' is more complex because it could be part of multi-bulk
397
+ when C_BULK
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
+ if cb = @redis_callbacks.shift
404
+ cb.call(@values)
405
+ @values = []
406
+ @multibulk_n = 0
407
+ end
408
+ end
409
+ else
410
+ if cb = @redis_callbacks.shift
411
+ cb.call(nil)
412
+ end
413
+ end
414
+ elsif @buffer.size >= data_len + 2 # buffer is full of expected data
415
+ if @multibulk_n > 0 # we're in the middle of a multibulk reply
416
+ @values << @buffer.slice!(0,data_len)
417
+ if @values.size == @multibulk_n # DING, we're done
418
+ if cb = @redis_callbacks.shift
419
+ cb.call(@values)
420
+ @values = []
421
+ @multibulk_n = 0
422
+ end
423
+ end
424
+ else # not multibulk
425
+ value = @buffer.slice!(0,data_len)
426
+ if cb = @redis_callbacks.shift
427
+ cb.call(value)
428
+ end
429
+ end
430
+ @buffer.slice!(0,2) # tossing \r\n
431
+ else # buffer isn't full or nil
432
+ # FYI, ParseError puts command back on head of buffer, waits for
433
+ # more data complete buffer
434
+ raise ParserError
435
+ end
436
+ #e.g. :8
437
+ when C_INT
438
+ if cb = @redis_callbacks.shift
439
+ cb.call( Integer(reply_args) )
440
+ end
441
+ #e.g. *2\r\n$1\r\na\r\n$1\r\nb\r\n
442
+ when C_MULTI
443
+ @multibulk_n = Integer(reply_args)
444
+ if @multibulk_n == -1
445
+ if cb = @redis_callbacks.shift
446
+ cb.call(nil)
447
+ end
448
+ end
449
+ #e.g. -MISSING
450
+ when C_ERR
451
+ @redis_callbacks.shift # throw away the cb?
452
+ if @err_cb
453
+ @err_cb.call(reply_args)
454
+ else
455
+ err = RedisError.new
456
+ err.code = reply_args
457
+ raise err, "Redis server returned error code: #{err.code}"
458
+ end
459
+ # Whu?
460
+ else
461
+ raise ProtocolError, "reply type not recognized: #{line.strip}"
462
+ end
463
+ end
464
+
465
+ def unbind
466
+ puts "*** unbinding" if $debug
467
+ if @connected or @reconnecting
468
+ EM.add_timer(1){ reconnect @host, @port }
469
+ @connected = false
470
+ @reconnecting = true
471
+ @deferred_status = nil
472
+ else
473
+ raise 'Unable to connect to memcached server'
474
+ end
475
+ end
476
+
477
+ end
478
+ end
479
+ end
@@ -0,0 +1,426 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/test_helper.rb")
2
+
3
+ EM.describe EM::Protocols::Redis, "connected to an empty db" do
4
+
5
+ before do
6
+ @c = EM::Protocols::Redis.connect
7
+ @c.select "14"
8
+ @c.flushdb
9
+ end
10
+
11
+ should "be able to set a string value" do
12
+ @c.set("foo", "bar") do |r|
13
+ r.should == "OK"
14
+ done
15
+ end
16
+ end
17
+
18
+ should "be able to increment the value of a string" do
19
+ @c.incr "foo" do |r|
20
+ r.should == 1
21
+ @c.incr "foo" do |r|
22
+ r.should == 2
23
+ done
24
+ end
25
+ end
26
+ end
27
+
28
+ should "be able to increment the value of a string by an amount" do
29
+ @c.incrby "foo", 10 do |r|
30
+ r.should == 10
31
+ done
32
+ end
33
+ end
34
+
35
+ should "be able to decrement the value of a string" do
36
+ @c.incr "foo" do |r|
37
+ r.should == 1
38
+ @c.decr "foo" do |r|
39
+ r.should == 0
40
+ done
41
+ end
42
+ end
43
+ end
44
+
45
+ should "be able to decrement the value of a string by an amount" do
46
+ @c.incrby "foo", 20 do |r|
47
+ r.should == 20
48
+ @c.decrby "foo", 10 do |r|
49
+ r.should == 10
50
+ done
51
+ end
52
+ end
53
+ end
54
+
55
+ should "be able to 'lpush' to a nonexistent list" do
56
+ @c.lpush("foo", "bar") do |r|
57
+ r.should == "OK"
58
+ done
59
+ end
60
+ end
61
+
62
+ should "be able to 'rpush' to a nonexistent list" do
63
+ @c.rpush("foo", "bar") do |r|
64
+ r.should == "OK"
65
+ done
66
+ end
67
+ end
68
+
69
+
70
+ should "be able to get the size of the database" do
71
+ @c.dbsize do |r|
72
+ r.should == 0
73
+ done
74
+ end
75
+ end
76
+
77
+ should "be able to add a member to a nonexistent set" do
78
+ @c.sadd("set_foo", "bar") do |r|
79
+ r.should == 1
80
+ done
81
+ end
82
+ end
83
+
84
+ should "be able to get info about the db as a hash" do
85
+ @c.info do |r|
86
+ r.should.key? "redis_version"
87
+ done
88
+ end
89
+ end
90
+
91
+ should "be able to save db" do
92
+ @c.save do |r|
93
+ r.should == "OK"
94
+ done
95
+ end
96
+ end
97
+
98
+ should "be able to save db in the background" do
99
+ @c.bgsave do |r|
100
+ r.should == "OK"
101
+ done
102
+ end
103
+ end
104
+
105
+
106
+ end
107
+
108
+ EM.describe EM::Protocols::Redis, "connected to a db containing some simple string-valued keys" do
109
+
110
+ before do
111
+ @c = EM::Protocols::Redis.connect
112
+ @c.select "14"
113
+ @c.flushdb
114
+ @c.set "a", "b"
115
+ @c.set "x", "y"
116
+ end
117
+
118
+ should "be able to fetch the values of multiple keys" do
119
+ @c.mget "a", "x" do |r|
120
+ r.should == ["b", "y"]
121
+ done
122
+ end
123
+ end
124
+
125
+ should "be able to fetch all the keys" do
126
+ @c.keys "*" do |r|
127
+ r.sort.should == ["a", "x"]
128
+ done
129
+ end
130
+ end
131
+
132
+ should "be able to set a value if a key doesn't exist" do
133
+ @c.setnx "a", "foo" do |r|
134
+ r.should == 0
135
+ @c.setnx "zzz", "foo" do |r|
136
+ r.should == 1
137
+ done
138
+ end
139
+ end
140
+ end
141
+
142
+ should "be able to test for the existence of a key" do
143
+ @c.exists "a" do |r|
144
+ r.should == 1
145
+ @c.exists "zzz" do |r|
146
+ r.should == 0
147
+ done
148
+ end
149
+ end
150
+ end
151
+
152
+ should "be able to delete a key" do
153
+ @c.del "a" do |r|
154
+ r.should == 1
155
+ @c.exists "a" do |r|
156
+ r.should == 0
157
+ @c.del "a" do |r|
158
+ r.should == 0
159
+ done
160
+ end
161
+ end
162
+ end
163
+ end
164
+
165
+ should "be able to detect the type of a key, existing or not" do
166
+ @c.type "a" do |r|
167
+ r.should == "string"
168
+ @c.type "zzz" do |r|
169
+ r.should == "none"
170
+ done
171
+ end
172
+ end
173
+ end
174
+
175
+ should "be able to rename a key" do
176
+ @c.rename "a", "x" do |r|
177
+ @c.get "x" do |r|
178
+ r.should == "b"
179
+ done
180
+ end
181
+ end
182
+ end
183
+
184
+ should "be able to rename a key unless it exists" do
185
+ @c.renamenx "a", "x" do |r|
186
+ r.should == 0
187
+ @c.renamenx "a", "zzz" do |r|
188
+ r.should == 1
189
+ @c.get "zzz" do |r|
190
+ r.should == "b"
191
+ done
192
+ end
193
+ end
194
+ end
195
+ end
196
+
197
+
198
+ end
199
+
200
+ EM.describe EM::Protocols::Redis, "connected to a db containing a list" do
201
+
202
+ before do
203
+ @c = EM::Protocols::Redis.connect
204
+ @c.select "14"
205
+ @c.flushdb
206
+ @c.lpush "foo", "c"
207
+ @c.lpush "foo", "b"
208
+ @c.lpush "foo", "a"
209
+ end
210
+
211
+ should "be able to 'lset' a list member and 'lindex' to retrieve it" do
212
+ @c.lset("foo", 1, "bar") do |r|
213
+ @c.lindex("foo", 1) do |r|
214
+ r.should == "bar"
215
+ done
216
+ end
217
+ end
218
+ end
219
+
220
+ should "be able to 'rpush' onto the tail of the list" do
221
+ @c.rpush "foo", "d" do |r|
222
+ r.should == "OK"
223
+ @c.rpop "foo" do |r|
224
+ r.should == "d"
225
+ done
226
+ end
227
+ end
228
+ end
229
+
230
+ should "be able to 'lpush' onto the head of the list" do
231
+ @c.lpush "foo", "d" do |r|
232
+ r.should == "OK"
233
+ @c.lpop "foo" do |r|
234
+ r.should == "d"
235
+ done
236
+ end
237
+ end
238
+ end
239
+
240
+ should "be able to 'rpop' off the tail of the list" do
241
+ @c.rpop("foo") do |r|
242
+ r.should == "c"
243
+ done
244
+ end
245
+ end
246
+
247
+ should "be able to 'lpop' off the tail of the list" do
248
+ @c.lpop("foo") do |r|
249
+ r.should == "a"
250
+ done
251
+ end
252
+ end
253
+
254
+ should "be able to get a range of values from a list" do
255
+ @c.lrange("foo", 0,1) do |r|
256
+ r.should == ["a", "b"]
257
+ done
258
+ end
259
+ end
260
+
261
+ should "be able to 'ltrim' a list" do
262
+ @c.ltrim("foo", 0,1) do |r|
263
+ r.should == "OK"
264
+ @c.llen("foo") do |r|
265
+ r.should == 2
266
+ done
267
+ end
268
+ end
269
+ end
270
+
271
+ should "be able to 'rem' a list element" do
272
+ @c.lrem("foo", "a", 0) do |r|
273
+ r.should == 1
274
+ @c.llen("foo") do |r|
275
+ r.should == 2
276
+ done
277
+ end
278
+ end
279
+ end
280
+
281
+ should "be able to detect the type of a list" do
282
+ @c.type "foo" do |r|
283
+ r.should == "list"
284
+ done
285
+ end
286
+ end
287
+
288
+ end
289
+
290
+ EM.describe EM::Protocols::Redis, "connected to a db containing two sets" do
291
+ before do
292
+ @c = EM::Protocols::Redis.connect
293
+ @c.select "14"
294
+ @c.flushdb
295
+ @c.sadd "foo", "a"
296
+ @c.sadd "foo", "b"
297
+ @c.sadd "foo", "c"
298
+ @c.sadd "bar", "c"
299
+ @c.sadd "bar", "d"
300
+ @c.sadd "bar", "e"
301
+ end
302
+
303
+ should "be able to find a set's cardinality" do
304
+ @c.scard("foo") do |r|
305
+ r.should == 3
306
+ done
307
+ end
308
+ end
309
+
310
+ should "be able to add a new member to a set unless it is a duplicate" do
311
+ @c.sadd("foo", "d") do |r|
312
+ r.should == 1 # success
313
+ @c.sadd("foo", "a") do |r|
314
+ r.should == 0 # failure
315
+ @c.scard("foo") do |r|
316
+ r.should == 4
317
+ done
318
+ end
319
+ end
320
+ end
321
+ end
322
+
323
+ should "be able to remove a set member if it exists" do
324
+ @c.srem("foo", "a") do |r|
325
+ r.should == 1
326
+ @c.srem("foo", "z") do |r|
327
+ r.should == 0
328
+ @c.scard("foo") do |r|
329
+ r.should == 2
330
+ done
331
+ end
332
+ end
333
+ end
334
+ end
335
+
336
+ should "be able to retrieve a set's members" do
337
+ @c.smembers("foo") do |r|
338
+ r.sort.should == ["a", "b", "c"]
339
+ done
340
+ end
341
+ end
342
+
343
+ should "be able to detect set membership" do
344
+ @c.sismember("foo", "a") do |r|
345
+ r.should == 1
346
+ @c.sismember("foo", "z") do |r|
347
+ r.should == 0
348
+ done
349
+ end
350
+ end
351
+ end
352
+
353
+ should "be able to find the sets' intersection" do
354
+ @c.sinter("foo", "bar") do |r|
355
+ r.should == ["c"]
356
+ done
357
+ end
358
+ end
359
+
360
+ should "be able to find and store the sets' intersection" do
361
+ @c.sinterstore("baz", "foo", "bar") do |r|
362
+ r.should == 1
363
+ @c.smembers("baz") do |r|
364
+ r.should == ["c"]
365
+ done
366
+ end
367
+ end
368
+ end
369
+
370
+ # UNION SET MANIP NOT IN RELEASE BUILDS YET
371
+ ###########################################
372
+ #
373
+ # should "be able to find the sets' union" do
374
+ # @c.sunion("foo", "bar") do |r|
375
+ # r.should == ["a","b","c","d","e"]
376
+ # done
377
+ # end
378
+ # end
379
+
380
+ # should "be able to find and store the sets' union" do
381
+ # @c.sunionstore("baz", "foo", "bar") do |r|
382
+ # r.should == "OK"
383
+ # @c.smembers("baz") do |r|
384
+ # r.should == ["a","b","c","d","e"]
385
+ # done
386
+ # end
387
+ # end
388
+ # end
389
+
390
+ should "be able to detect the type of a set" do
391
+ @c.type "foo" do |r|
392
+ r.should == "set"
393
+ done
394
+ end
395
+ end
396
+
397
+ end
398
+
399
+
400
+ EM.describe EM::Protocols::Redis, "connected to a db containing three linked lists" do
401
+ before do
402
+ @c = EM::Protocols::Redis.connect
403
+ @c.select "14"
404
+ @c.flushdb
405
+ @c.rpush "foo", "a"
406
+ @c.rpush "foo", "b"
407
+ @c.set "a_sort", "2"
408
+ @c.set "b_sort", "1"
409
+ @c.set "a_data", "foo"
410
+ @c.set "b_data", "bar"
411
+ end
412
+
413
+ should "be able to collate a sorted set of data" do
414
+ @c.sort("foo", "*_sort", nil, nil, "*_data") do |r|
415
+ r.should == ["bar", "foo"]
416
+ done
417
+ end
418
+ end
419
+
420
+ should "be able to get keys selectively" do
421
+ @c.keys "a_*" do |r|
422
+ r.should == ["a_sort", "a_data"]
423
+ done
424
+ end
425
+ end
426
+ end
@@ -0,0 +1,133 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/test_helper.rb")
2
+
3
+ EM.describe EM::Protocols::Redis do
4
+
5
+ before do
6
+ @c = TestConnection.new
7
+ end
8
+
9
+ # Inline request protocol
10
+ should 'send inline commands correctly' do
11
+ @c.inline_command("GET", 'a')
12
+ @c.sent_data.should == "GET a\r\n"
13
+ done
14
+ end
15
+
16
+ should "space-separate multiple inline arguments" do
17
+ @c.inline_command("GET", 'a', 'b', 'c')
18
+ @c.sent_data.should == "GET a b c\r\n"
19
+ done
20
+ end
21
+
22
+ # Multiline request protocol
23
+ should "send multiline commands correctly" do
24
+ @c.multiline_command("SET", "foo", "abc")
25
+ @c.sent_data.should == "SET foo 3\r\nabc\r\n"
26
+ done
27
+ end
28
+
29
+ should "send integers in multiline commands correctly" do
30
+ @c.multiline_command("SET", "foo", 1_000_000)
31
+ @c.sent_data.should == "SET foo 7\r\n1000000\r\n"
32
+ done
33
+ end
34
+
35
+ # Specific calls
36
+ #
37
+ # SORT
38
+ should "send sort command" do
39
+ @c.sort "foo"
40
+ @c.sent_data.should == "SORT foo\r\n"
41
+ done
42
+ end
43
+
44
+ should "send sort command with all optional parameters" do
45
+ @c.sort "foo", "foo_sort_*", 0, 10, "data_*", true, true
46
+ @c.sent_data.should == "SORT foo BY foo_sort_* LIMIT 0 10 GET data_* DESC ALPHA\r\n"
47
+ done
48
+ end
49
+
50
+ should "parse keys response into an array" do
51
+ @c.keys "*" do |resp|
52
+ resp.should == ["a","b","c"]
53
+ done
54
+ end
55
+ @c.receive_data "$5\r\na b c\r\n"
56
+ end
57
+
58
+
59
+ # Inline response
60
+ should "parse an inline response" do
61
+ @c.inline_command("PING") do |resp|
62
+ resp.should == "OK"
63
+ done
64
+ end
65
+ @c.receive_data "+OK\r\n"
66
+ end
67
+
68
+ should "parse an inline integer response" do
69
+ @c.inline_command("EXISTS") do |resp|
70
+ resp.should == 0
71
+ done
72
+ end
73
+ @c.receive_data ":0\r\n"
74
+ end
75
+
76
+ should "parse an inline error response" do
77
+ lambda do
78
+ @c.inline_command("BLARG")
79
+ @c.receive_data "-FAIL\r\n"
80
+ end.should.raise(EM::P::Redis::RedisError)
81
+ done
82
+ end
83
+
84
+ should "trigger a given error callback for inline error response instead of raising an error" do
85
+ lambda do
86
+ @c.inline_command("BLARG")
87
+ @c.on_error {|code| code.should == "FAIL"; done }
88
+ @c.receive_data "-FAIL\r\n"
89
+ end.should.not.raise(EM::P::Redis::RedisError)
90
+ done
91
+ end
92
+
93
+ # Bulk response
94
+ should "parse a bulk response" do
95
+ @c.inline_command("GET", "foo") do |resp|
96
+ resp.should == "bar"
97
+ done
98
+ end
99
+ @c.receive_data "$3\r\n"
100
+ @c.receive_data "bar\r\n"
101
+ end
102
+
103
+ should "distinguish nil in a bulk response" do
104
+ @c.inline_command("GET", "bar") do |resp|
105
+ resp.should == nil
106
+ end
107
+ @c.receive_data "$-1\r\n"
108
+ end
109
+
110
+ # Multi-bulk response
111
+
112
+ should "parse a multi-bulk response" do
113
+ @c.inline_command "RANGE", 0, 10 do |resp|
114
+ resp.should == ["a", "b", "foo"]
115
+ done
116
+ end
117
+ @c.receive_data "*3\r\n"
118
+ @c.receive_data "$1\r\na\r\n"
119
+ @c.receive_data "$1\r\nb\r\n"
120
+ @c.receive_data "$3\r\nfoo\r\n"
121
+ end
122
+
123
+ should "distinguish nil in a multi-bulk response" do
124
+ @c.inline_command "RANGE", 0, 10 do |resp|
125
+ resp.should == ["a", nil, "foo"]
126
+ done
127
+ end
128
+ @c.receive_data "*3\r\n"
129
+ @c.receive_data "$1\r\na\r\n"
130
+ @c.receive_data "$-1\r\n"
131
+ @c.receive_data "$3\r\nfoo\r\n"
132
+ end
133
+ end
@@ -0,0 +1,18 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../lib/em-redis")
2
+ require 'em-spec/bacon'
3
+
4
+ EM.spec_backend = EventMachine::Spec::Bacon
5
+
6
+ class TestConnection
7
+ include EM::P::Redis
8
+ def send_data data
9
+ sent_data << data
10
+ end
11
+ def sent_data
12
+ @sent_data ||= ''
13
+ end
14
+
15
+ def initialize
16
+ connection_completed
17
+ end
18
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: em-redis
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Jonathan Broad
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-12-15 00:00:00 +03:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: bacon
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description: |-
26
+ 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.
27
+ 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).
28
+
29
+ This library is only useful when used as part of an application that relies on
30
+ Event Machine's event loop. It implements an EM-based client protocol, which
31
+ leverages the non-blocking nature of the EM interface to acheive significant
32
+ parallelization without threads.
33
+
34
+ WARNING: this library is my first attempt to write an evented client protocol,
35
+ and isn't currently used in production anywhere. All that bit in the license
36
+ about not being warranted to work for any particular purpose really applies.
37
+ email: jonathan@relativepath.org
38
+ executables: []
39
+
40
+ extensions: []
41
+
42
+ extra_rdoc_files:
43
+ - History.txt
44
+ - README.rdoc
45
+ files:
46
+ - History.txt
47
+ - Manifest.txt
48
+ - README.rdoc
49
+ - em-redis.gemspec
50
+ - Rakefile
51
+ - lib/em-redis.rb
52
+ - lib/em-redis/redis_protocol.rb
53
+ - spec/test_helper.rb
54
+ - spec/live_redis_protocol_spec.rb
55
+ - spec/redis_protocol_spec.rb
56
+ has_rdoc: true
57
+ homepage:
58
+ licenses: []
59
+
60
+ post_install_message:
61
+ rdoc_options:
62
+ - --main
63
+ - README.rdoc
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: "0"
71
+ version:
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: "0"
77
+ version:
78
+ requirements: []
79
+
80
+ rubyforge_project: em-redis
81
+ rubygems_version: 1.3.5
82
+ signing_key:
83
+ specification_version: 3
84
+ summary: An EventMachine[http://rubyeventmachine
85
+ test_files: []
86
+