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 +11 -0
- data/Manifest.txt +10 -0
- data/README.rdoc +94 -0
- data/Rakefile +26 -0
- data/em-redis.gemspec +35 -0
- data/lib/em-redis.rb +36 -0
- data/lib/em-redis/redis_protocol.rb +479 -0
- data/spec/live_redis_protocol_spec.rb +426 -0
- data/spec/redis_protocol_spec.rb +133 -0
- data/spec/test_helper.rb +18 -0
- metadata +86 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
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
|
data/spec/test_helper.rb
ADDED
@@ -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
|
+
|