ruby-redis-portertech 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +9 -0
- data/README.md +80 -0
- data/bin/ruby-redis +12 -0
- data/lib/redis.rb +13 -0
- data/lib/redis/bin.rb +127 -0
- data/lib/redis/client.rb +213 -0
- data/lib/redis/config.rb +49 -0
- data/lib/redis/connection.rb +30 -0
- data/lib/redis/database.rb +133 -0
- data/lib/redis/hashes.rb +70 -0
- data/lib/redis/keys.rb +155 -0
- data/lib/redis/lists.rb +225 -0
- data/lib/redis/logger.rb +69 -0
- data/lib/redis/protocol.rb +107 -0
- data/lib/redis/pubsub.rb +150 -0
- data/lib/redis/reader.rb +175 -0
- data/lib/redis/sender.rb +47 -0
- data/lib/redis/server.rb +60 -0
- data/lib/redis/sets.rb +104 -0
- data/lib/redis/strict.rb +65 -0
- data/lib/redis/strings.rb +142 -0
- data/lib/redis/synchrony.rb +51 -0
- data/lib/redis/version.rb +5 -0
- data/lib/redis/zsets.rb +279 -0
- metadata +120 -0
data/LICENSE
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
Copyright (c) 2011, David Turnbull et al
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
5
|
+
|
6
|
+
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
7
|
+
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
8
|
+
|
9
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
This project was started because I needed an authenticating and routable
|
2
|
+
proxy for Redis. The main feature is a high performance, eventable, pure Ruby
|
3
|
+
implementation of the complete Redis wire protocol using the same interface as
|
4
|
+
hiredis/reader.
|
5
|
+
|
6
|
+
### Ruby Gem
|
7
|
+
|
8
|
+
# Best with Ruby 1.9.2; works with 1.8.7, JRuby, and Rubinius.
|
9
|
+
gem install ruby-redis
|
10
|
+
# Runs a server that looks and feels like the C redis-server
|
11
|
+
ruby-redis
|
12
|
+
|
13
|
+
### Client example
|
14
|
+
|
15
|
+
require 'redis'
|
16
|
+
EventMachine.run {
|
17
|
+
redis = EventMachine.connect '127.0.0.1', 6379, Redis::Client
|
18
|
+
# Subscribe and publish messages will call here
|
19
|
+
redis.on_pubsub do |message|
|
20
|
+
# case message[0]
|
21
|
+
# when 'psubscribe' ...
|
22
|
+
end
|
23
|
+
# All commands implemented uniformly with method_missing
|
24
|
+
# Returns instance of Redis::Command < EventMachine::Deferrable
|
25
|
+
# Pipelining is implicit
|
26
|
+
redis.set :pi, 3.14159
|
27
|
+
redis.get('pi') do |result|
|
28
|
+
p result
|
29
|
+
end
|
30
|
+
redis.blpop('mylist', 1).callback do |result|
|
31
|
+
p result
|
32
|
+
EM.stop
|
33
|
+
end.errback do |e|
|
34
|
+
EM.stop
|
35
|
+
raise e
|
36
|
+
end
|
37
|
+
}
|
38
|
+
|
39
|
+
|
40
|
+
### Using hiredis/reader (only affects clients)
|
41
|
+
|
42
|
+
# require it before you connect
|
43
|
+
require 'hiredis/reader'
|
44
|
+
|
45
|
+
### Fibers example (compatible with em-synchrony)
|
46
|
+
|
47
|
+
require 'redis/synchrony'
|
48
|
+
Redis.synchrony {
|
49
|
+
# Be sure to pipeline commands when you can
|
50
|
+
redis = EventMachine.connect '127.0.0.1', 6379, Redis::Client
|
51
|
+
# synchronized requests will return result or raise exception
|
52
|
+
sync = redis.synchrony
|
53
|
+
# repeat transaction until success
|
54
|
+
reply = check = nil
|
55
|
+
until reply
|
56
|
+
redis.watch 'mykey'
|
57
|
+
x = sync.get('mykey').to_i
|
58
|
+
redis.multi
|
59
|
+
redis.set 'mykey', x + 1
|
60
|
+
redis.badcmd
|
61
|
+
redis.get('mykey') {|result| check=result}
|
62
|
+
reply = sync.exec
|
63
|
+
end
|
64
|
+
redis.close
|
65
|
+
EM.stop
|
66
|
+
p reply, check # ["OK", #<RuntimeError>, "4"], "4"
|
67
|
+
}
|
68
|
+
|
69
|
+
|
70
|
+
### Ruby to Redis type conversions
|
71
|
+
|
72
|
+
String === Status reply
|
73
|
+
RuntimeError === Error reply
|
74
|
+
String === Bulk reply
|
75
|
+
Integer === Integer reply
|
76
|
+
Array === Multi-bulk reply
|
77
|
+
Hash === Multi-bulk reply to hgetall
|
78
|
+
NilClass === Nil element or nil multi-bulk
|
79
|
+
TrueClass === :1
|
80
|
+
FalseClass === :0
|
data/bin/ruby-redis
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
begin
|
3
|
+
require 'rubygems'
|
4
|
+
rescue LoadError
|
5
|
+
end
|
6
|
+
direct_bin_filename = File.expand_path '../lib/redis/bin', File.dirname(__FILE__)
|
7
|
+
if File.exist? direct_bin_filename + '.rb'
|
8
|
+
require direct_bin_filename
|
9
|
+
else
|
10
|
+
require 'redis/bin'
|
11
|
+
end
|
12
|
+
Redis::Bin.server
|
data/lib/redis.rb
ADDED
data/lib/redis/bin.rb
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
%w{reader sender version config logger strict connection protocol database
|
2
|
+
server keys strings lists sets zsets hashes pubsub}.each do |file|
|
3
|
+
require File.expand_path file, File.dirname(__FILE__)
|
4
|
+
end
|
5
|
+
|
6
|
+
module Redis
|
7
|
+
class Bin
|
8
|
+
|
9
|
+
class RubyRedisServer < EventMachine::Connection
|
10
|
+
|
11
|
+
include Strict
|
12
|
+
include Connection
|
13
|
+
include Protocol
|
14
|
+
include Sender
|
15
|
+
|
16
|
+
def initialize logger, databases, config={}
|
17
|
+
@logger = logger
|
18
|
+
@databases = databases
|
19
|
+
@database = databases[0]
|
20
|
+
@config = config
|
21
|
+
super
|
22
|
+
end
|
23
|
+
|
24
|
+
def post_init
|
25
|
+
authorized nil
|
26
|
+
set_comm_inactivity_timeout @config[:timeout]
|
27
|
+
end
|
28
|
+
|
29
|
+
def authorized password
|
30
|
+
return true if @authorized
|
31
|
+
return false unless @config[:requirepass] == password
|
32
|
+
extend Server
|
33
|
+
extend Keys
|
34
|
+
extend Strings
|
35
|
+
extend Lists
|
36
|
+
extend Sets
|
37
|
+
extend ZSets
|
38
|
+
extend Hashes
|
39
|
+
extend PubSub
|
40
|
+
@authorized = true
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.server
|
46
|
+
|
47
|
+
if ARGV==['-v'] or ARGV==['--version']
|
48
|
+
print "Redis server version %s (Ruby)\n" % Redis::VERSION
|
49
|
+
exit 0
|
50
|
+
end
|
51
|
+
|
52
|
+
if ARGV==['--help'] or ARGV.size > 1
|
53
|
+
STDERR.print "Usage: ruby-redis [/path/to/redis.conf]\n"
|
54
|
+
STDERR.print " ruby-redis - (read config from stdin)\n"
|
55
|
+
exit 1
|
56
|
+
end
|
57
|
+
|
58
|
+
show_no_config_warning = (ARGV.size == 0)
|
59
|
+
|
60
|
+
config = Config.new(ARGV.empty? ? [] : ARGF)
|
61
|
+
|
62
|
+
Dir.chdir config[:dir]
|
63
|
+
|
64
|
+
if config[:logfile] == 'stdout'
|
65
|
+
logger = Logger.new STDOUT
|
66
|
+
else
|
67
|
+
logger = Logger.new config[:logfile]
|
68
|
+
end
|
69
|
+
|
70
|
+
if config[:loglevel] == 'debug'
|
71
|
+
logger.level = Logger::DEBUG
|
72
|
+
elsif config[:loglevel] == 'notice'
|
73
|
+
logger.level = Logger::NOTICE
|
74
|
+
elsif config[:loglevel] == 'warning'
|
75
|
+
logger.level = Logger::WARNING
|
76
|
+
elsif config[:loglevel] != 'verbose'
|
77
|
+
raise "Invalid log level. Must be one of debug, notice, warning, verbose."
|
78
|
+
else
|
79
|
+
logger.level = Logger::INFO
|
80
|
+
end
|
81
|
+
|
82
|
+
if show_no_config_warning
|
83
|
+
logger.warn "Warning: no config file specified, using the default config. In order to specify a config file use 'ruby-redis /path/to/redis.conf'"
|
84
|
+
end
|
85
|
+
|
86
|
+
if config[:daemonize]
|
87
|
+
exit!(0) if fork
|
88
|
+
Process::setsid
|
89
|
+
exit!(0) if fork
|
90
|
+
STDIN.reopen("/dev/null")
|
91
|
+
STDOUT.reopen("/dev/null", "w")
|
92
|
+
STDERR.reopen("/dev/null", "w")
|
93
|
+
begin
|
94
|
+
File.open(config[:pidfile], 'w') do |io|
|
95
|
+
io.write "%d\n" % Process.pid
|
96
|
+
end
|
97
|
+
at_exit do
|
98
|
+
File.delete config[:pidfile] if File.exist? config[:pidfile]
|
99
|
+
end
|
100
|
+
rescue StandardError => e
|
101
|
+
logger.error e.message
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
EventMachine.run {
|
106
|
+
|
107
|
+
databases = Array.new(config[:databases]) {Database.new}
|
108
|
+
started_message = "Server started, Ruby Redis version %s" % Redis::VERSION
|
109
|
+
|
110
|
+
if config[:unixsocket]
|
111
|
+
EventMachine::start_server config[:unixsocket], RubyRedisServer, logger, databases, config
|
112
|
+
logger.notice started_message
|
113
|
+
logger.notice "The server is now ready to accept connections at %s" % config[:unixsocket]
|
114
|
+
else
|
115
|
+
EventMachine::start_server config[:bind], config[:port], RubyRedisServer, logger, databases, config
|
116
|
+
logger.notice started_message
|
117
|
+
logger.notice "The server is now ready to accept connections on port %d" % config[:port]
|
118
|
+
end
|
119
|
+
|
120
|
+
# The test suite blocks until it gets the pid from the log.
|
121
|
+
logger.flush
|
122
|
+
|
123
|
+
}
|
124
|
+
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
data/lib/redis/client.rb
ADDED
@@ -0,0 +1,213 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
%w{reader sender}.each do |file|
|
3
|
+
require File.expand_path file, File.dirname(__FILE__)
|
4
|
+
end
|
5
|
+
|
6
|
+
module Redis
|
7
|
+
class Client < EventMachine::Connection
|
8
|
+
|
9
|
+
include Sender
|
10
|
+
|
11
|
+
class Command
|
12
|
+
include EventMachine::Deferrable
|
13
|
+
# EventMachine::Deferrable older than 1.0.0.beta.4 doesn't return self
|
14
|
+
# EventMachine::Completion doesn't return self in any version
|
15
|
+
test = self.new
|
16
|
+
unless self === test.callback{}
|
17
|
+
def callback; super; self; end
|
18
|
+
def errback; super; self; end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize *ignore_args
|
23
|
+
if defined? Hiredis and defined? Hiredis::Reader
|
24
|
+
@reader = Hiredis::Reader.new
|
25
|
+
else
|
26
|
+
@reader = Reader.new
|
27
|
+
end
|
28
|
+
@queue = []
|
29
|
+
@multi = nil
|
30
|
+
@pubsub_callback = proc{}
|
31
|
+
end
|
32
|
+
|
33
|
+
def connection_completed
|
34
|
+
@connected = true
|
35
|
+
@port, @host = Socket.unpack_sockaddr_in(get_peername)
|
36
|
+
end
|
37
|
+
|
38
|
+
def close
|
39
|
+
@closing_connection = true
|
40
|
+
close_connection_after_writing
|
41
|
+
end
|
42
|
+
|
43
|
+
def unbind
|
44
|
+
unless !@connected || @closing_connection
|
45
|
+
EM.add_timer(1) do
|
46
|
+
reconnect(@host, @port)
|
47
|
+
end
|
48
|
+
else
|
49
|
+
until @queue.empty?
|
50
|
+
@queue.shift.fail RuntimeError.new 'connection closed'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# This is simple and fast but doesn't test for programming errors.
|
56
|
+
# Don't send non-pubsub commands while subscribed and you're fine.
|
57
|
+
# Subclass Client and/or create a defensive layer if you need to.
|
58
|
+
def on_pubsub &block
|
59
|
+
@pubsub_callback = block
|
60
|
+
end
|
61
|
+
|
62
|
+
def receive_data data
|
63
|
+
@reader.feed data
|
64
|
+
until (
|
65
|
+
begin
|
66
|
+
data = @reader.gets
|
67
|
+
rescue StandardError => e
|
68
|
+
@queue.shift.fail e unless @queue.empty?
|
69
|
+
close_connection
|
70
|
+
data = false
|
71
|
+
end
|
72
|
+
) == false
|
73
|
+
deferrable = @queue.shift
|
74
|
+
if deferrable
|
75
|
+
if Exception === data
|
76
|
+
deferrable.fail data
|
77
|
+
else
|
78
|
+
deferrable.succeed data
|
79
|
+
end
|
80
|
+
else
|
81
|
+
@pubsub_callback.call data
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def method_missing method, *args, &block
|
87
|
+
if @multi
|
88
|
+
for_queue = new_command true, false, method, *args
|
89
|
+
command = new_command false, true, method, &block
|
90
|
+
callback_multi = @multi
|
91
|
+
for_queue.callback do |status|
|
92
|
+
callback_multi << command
|
93
|
+
end
|
94
|
+
for_queue.errback do |err|
|
95
|
+
callback_multi << err
|
96
|
+
command.fail err
|
97
|
+
end
|
98
|
+
else
|
99
|
+
command = new_command true, true, method, *args, &block
|
100
|
+
end
|
101
|
+
command
|
102
|
+
end
|
103
|
+
|
104
|
+
def in_multi?
|
105
|
+
!!@multi
|
106
|
+
end
|
107
|
+
|
108
|
+
def multi *args, &block
|
109
|
+
@multi ||= []
|
110
|
+
new_command true, true, :multi, *args, &block
|
111
|
+
end
|
112
|
+
|
113
|
+
def exec *args, &block
|
114
|
+
redis_exec = new_command true, true, :exec, *args
|
115
|
+
callback_multi = @multi
|
116
|
+
@multi = nil
|
117
|
+
redis_exec.callback do |results|
|
118
|
+
if results
|
119
|
+
normalized_results = []
|
120
|
+
callback_multi.each do |command|
|
121
|
+
if Exception === command
|
122
|
+
normalized_results << command
|
123
|
+
else
|
124
|
+
result = results.shift
|
125
|
+
normalized_results << result
|
126
|
+
if Exception === result
|
127
|
+
command.fail result
|
128
|
+
else
|
129
|
+
command.succeed result
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
redis_exec.succeed normalized_results
|
134
|
+
end
|
135
|
+
end
|
136
|
+
redis_exec.callback &block if block_given?
|
137
|
+
redis_exec
|
138
|
+
end
|
139
|
+
|
140
|
+
# Some data is best transformed into a Ruby type. You can set up global
|
141
|
+
# transforms here that are automatically attached to command callbacks.
|
142
|
+
# Redis::Client.transforms[:mycustom1] = Redis::Client.transforms[:exists] # boolean
|
143
|
+
# Redis::Client.transforms[:mycustom2] = proc { |data| MyType.new data }
|
144
|
+
# Redis::Client.transforms.delete :hgetall # if you prefer the array
|
145
|
+
def self.transforms
|
146
|
+
@@transforms ||= lambda {
|
147
|
+
boolean = lambda { |tf| tf[0] == 1 ? true : false }
|
148
|
+
hash = lambda { |hash| Hash[*hash] }
|
149
|
+
pubsub = lambda { |msg| lambda { msg } }
|
150
|
+
{
|
151
|
+
#pubsub
|
152
|
+
:subscribe => pubsub,
|
153
|
+
:psubscribe => pubsub,
|
154
|
+
:unsubscribe => pubsub,
|
155
|
+
:punsubscribe => pubsub,
|
156
|
+
#keys
|
157
|
+
:exists => boolean,
|
158
|
+
:expire => boolean,
|
159
|
+
:expireat => boolean,
|
160
|
+
:move => boolean,
|
161
|
+
:persist => boolean,
|
162
|
+
:renamenx => boolean,
|
163
|
+
#strings
|
164
|
+
:msetnx => boolean,
|
165
|
+
:setnx => boolean,
|
166
|
+
#hashes
|
167
|
+
:hexists => boolean,
|
168
|
+
:hgetall => hash,
|
169
|
+
:hset => boolean,
|
170
|
+
:hsetnx => boolean,
|
171
|
+
#sets
|
172
|
+
:sismember => boolean,
|
173
|
+
:smove => boolean,
|
174
|
+
:srem => boolean,
|
175
|
+
#zsets
|
176
|
+
:zrem => boolean,
|
177
|
+
}
|
178
|
+
}.call
|
179
|
+
end
|
180
|
+
|
181
|
+
private
|
182
|
+
|
183
|
+
def new_command do_send, do_transform, method, *args, &block
|
184
|
+
command = Command.new
|
185
|
+
if do_send
|
186
|
+
send_redis args.reduce([method]){ |arr, arg|
|
187
|
+
if Hash === arg
|
188
|
+
arr += arg.to_a.flatten 1
|
189
|
+
else
|
190
|
+
arr << arg
|
191
|
+
end
|
192
|
+
}
|
193
|
+
@queue.push command
|
194
|
+
end
|
195
|
+
if do_transform
|
196
|
+
if transform = self.class.transforms[method]
|
197
|
+
command.callback do |data|
|
198
|
+
result = transform.call data
|
199
|
+
if Proc === result
|
200
|
+
command.succeed nil
|
201
|
+
@pubsub_callback.call result.call
|
202
|
+
else
|
203
|
+
command.succeed result
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
command.callback &block if block_given?
|
209
|
+
command
|
210
|
+
end
|
211
|
+
|
212
|
+
end
|
213
|
+
end
|