cs-em-hiredis 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/.rspec +1 -0
- data/Gemfile +3 -0
- data/LICENCE +19 -0
- data/README.md +85 -0
- data/Rakefile +11 -0
- data/cs-em-hiredis.gemspec +25 -0
- data/examples/pubsub.rb +52 -0
- data/lib/em-hiredis.rb +49 -0
- data/lib/em-hiredis/base_client.rb +199 -0
- data/lib/em-hiredis/client.rb +51 -0
- data/lib/em-hiredis/connection.rb +70 -0
- data/lib/em-hiredis/event_emitter.rb +29 -0
- data/lib/em-hiredis/lock.rb +89 -0
- data/lib/em-hiredis/pubsub_client.rb +187 -0
- data/lib/em-hiredis/version.rb +5 -0
- data/spec/base_client_spec.rb +115 -0
- data/spec/connection_spec.rb +56 -0
- data/spec/live_redis_protocol_spec.rb +527 -0
- data/spec/pubsub_spec.rb +314 -0
- data/spec/redis_commands_spec.rb +910 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/support/connection_helper.rb +11 -0
- data/spec/support/redis_mock.rb +65 -0
- data/spec/url_param_spec.rb +43 -0
- metadata +125 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/Gemfile
ADDED
data/LICENCE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (C) 2011 by Martyn Loughran
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
Getting started
|
2
|
+
===============
|
3
|
+
|
4
|
+
Connect to redis:
|
5
|
+
|
6
|
+
require 'em-hiredis'
|
7
|
+
redis = EM::Hiredis.connect
|
8
|
+
|
9
|
+
Or, connect to redis with a redis URL (for a different host, port, password, DB)
|
10
|
+
|
11
|
+
redis = EM::Hiredis.connect("redis://:secretpassword@example.com:9000/4")
|
12
|
+
|
13
|
+
The client is a deferrable which succeeds when the underlying connection is established so you can bind to this. This isn't necessary however - any commands sent before the connection is established (or while reconnecting) will be sent to redis on connect.
|
14
|
+
|
15
|
+
redis.callback { puts "Redis now connected" }
|
16
|
+
|
17
|
+
All redis commands are available without any remapping of names
|
18
|
+
|
19
|
+
redis.set('foo', 'bar').callback {
|
20
|
+
redis.get('foo').callback { |value|
|
21
|
+
p [:returned, value]
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
As a shortcut, if you're only interested in binding to the success case you can simply provide a block to any command
|
26
|
+
|
27
|
+
redis.get('foo') { |value|
|
28
|
+
p [:returned, value]
|
29
|
+
}
|
30
|
+
|
31
|
+
Handling failure
|
32
|
+
----------------
|
33
|
+
|
34
|
+
All commands return a deferrable. In the case that redis replies with an error (for example you called a hash operation against a set), or in the case that the redis connection is broken before the command returns, the deferrable will fail. If you care about the failure case you should bind to the errback - for example:
|
35
|
+
|
36
|
+
redis.sadd('aset', 'member').callback {
|
37
|
+
response_deferrable = redis.hget('aset', 'member')
|
38
|
+
response_deferrable.errback { |e|
|
39
|
+
p e # => #<RuntimeError: ERR Operation against a key holding the wrong kind of value>
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
Pubsub
|
44
|
+
------
|
45
|
+
|
46
|
+
This example should explain things. Once a redis connection is in a pubsub state, you must make sure you only send pubsub commands.
|
47
|
+
|
48
|
+
redis = EM::Hiredis.connect
|
49
|
+
subscriber = EM::Hiredis.connect
|
50
|
+
|
51
|
+
subscriber.subscribe('bar.0')
|
52
|
+
subscriber.psubscribe('bar.*')
|
53
|
+
|
54
|
+
subscriber.on(:message) { |channel, message|
|
55
|
+
p [:message, channel, message]
|
56
|
+
}
|
57
|
+
|
58
|
+
subscriber.on(:pmessage) { |key, channel, message|
|
59
|
+
p [:pmessage, key, channel, message]
|
60
|
+
}
|
61
|
+
|
62
|
+
EM.add_periodic_timer(1) {
|
63
|
+
redis.publish("bar.#{rand(2)}", "hello").errback { |e|
|
64
|
+
p [:publisherror, e]
|
65
|
+
}
|
66
|
+
}
|
67
|
+
|
68
|
+
Hacking
|
69
|
+
-------
|
70
|
+
|
71
|
+
Hacking on em-hiredis is pretty simple, make sure you have Bundler installed:
|
72
|
+
|
73
|
+
gem install bundler
|
74
|
+
bundle
|
75
|
+
|
76
|
+
In order to run the tests you need to have a local redis server running on port 6379. Run all the tests:
|
77
|
+
|
78
|
+
# WARNING: The tests call flushdb on db 9 - this clears all keys!
|
79
|
+
bundle exec rake
|
80
|
+
|
81
|
+
To run an individual test:
|
82
|
+
|
83
|
+
bundle exec rspec spec/redis_commands_spec.rb
|
84
|
+
|
85
|
+
Many thanks to the em-redis gem for getting this gem bootstrapped with some tests.
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "em-hiredis/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "cs-em-hiredis"
|
7
|
+
s.version = EventMachine::Hiredis::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Martyn Loughran", "Yuri Niyazov"]
|
10
|
+
s.email = ["me@mloughran.com", "yuri.niyazov@gmail.com"]
|
11
|
+
s.homepage = "http://github.com/yn/em-hiredis"
|
12
|
+
s.summary = %q{Eventmachine redis client}
|
13
|
+
s.description = %q{Eventmachine redis client using hiredis native parser}
|
14
|
+
|
15
|
+
s.add_dependency 'hiredis', '~> 0.4.0'
|
16
|
+
|
17
|
+
s.add_development_dependency 'em-spec', '~> 0.2.5'
|
18
|
+
s.add_development_dependency 'rspec', '~> 2.6.0'
|
19
|
+
s.add_development_dependency 'rake'
|
20
|
+
|
21
|
+
s.files = `git ls-files`.split("\n")
|
22
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
23
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
24
|
+
s.require_paths = ["lib"]
|
25
|
+
end
|
data/examples/pubsub.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
$:.unshift(File.expand_path('../../lib', __FILE__))
|
3
|
+
require 'em-hiredis'
|
4
|
+
|
5
|
+
EM.run {
|
6
|
+
redis = EM::Hiredis.connect
|
7
|
+
|
8
|
+
# If you pass a block to subscribe it will be called whenever a message
|
9
|
+
# is received on this channel
|
10
|
+
redis.pubsub.subscribe('foo') { |message|
|
11
|
+
puts "Block received #{message}"
|
12
|
+
}
|
13
|
+
|
14
|
+
# You can also pass any other object which responds to call if you wish
|
15
|
+
callback = Proc.new { |message|
|
16
|
+
"Proc received #{message}"
|
17
|
+
}
|
18
|
+
df = redis.pubsub.subscribe('foo', callback)
|
19
|
+
|
20
|
+
# All calls return a deferrable
|
21
|
+
df.callback { |reply|
|
22
|
+
p [:subscription_succeeded, reply]
|
23
|
+
}
|
24
|
+
|
25
|
+
# Passing such an object is useful if you want to unsubscribe
|
26
|
+
redis.pubsub.unsubscribe_proc('foo', callback)
|
27
|
+
|
28
|
+
# Or if you want to call a method on a certain object
|
29
|
+
class Thing
|
30
|
+
def receive_message(message)
|
31
|
+
puts "Thing received #{message}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
redis.pubsub.subscribe('bar', Thing.new.method(:receive_message))
|
35
|
+
|
36
|
+
# You can also get all the following raw events:
|
37
|
+
# message pmessage subscribe unsubscribe psubscribe punsubscribe
|
38
|
+
redis.pubsub.on(:message) { |channel, message|
|
39
|
+
p [:message_received, channel, message]
|
40
|
+
}
|
41
|
+
redis.pubsub.on(:unsubscribe) { |channel, remaining_subscriptions|
|
42
|
+
p [:unsubscribe_happened, channel, remaining_subscriptions]
|
43
|
+
}
|
44
|
+
|
45
|
+
EM.add_timer(1) {
|
46
|
+
# You can also unsubscribe completely from a channel
|
47
|
+
redis.pubsub.unsubscribe('foo')
|
48
|
+
|
49
|
+
# Publishing events
|
50
|
+
redis.publish('bar', 'Hello')
|
51
|
+
}
|
52
|
+
}
|
data/lib/em-hiredis.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
module EventMachine
|
4
|
+
module Hiredis
|
5
|
+
class Error < RuntimeError
|
6
|
+
# In the case of error responses from Redis, the RuntimeError returned
|
7
|
+
# by ::Hiredis will be wrapped
|
8
|
+
attr_accessor :redis_error
|
9
|
+
end
|
10
|
+
|
11
|
+
class << self
|
12
|
+
attr_accessor :reconnect_timeout
|
13
|
+
end
|
14
|
+
self.reconnect_timeout = 0.5
|
15
|
+
|
16
|
+
def self.setup(uri = nil)
|
17
|
+
uri = uri || ENV["REDIS_URL"] || "redis://127.0.0.1:6379/0"
|
18
|
+
client = Client.new
|
19
|
+
client.configure(uri)
|
20
|
+
client
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.connect(uri = nil)
|
24
|
+
client = setup(uri)
|
25
|
+
client.connect
|
26
|
+
client
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.logger=(logger)
|
30
|
+
@@logger = logger
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.logger
|
34
|
+
@@logger ||= begin
|
35
|
+
require 'logger'
|
36
|
+
log = Logger.new(STDOUT)
|
37
|
+
log.level = Logger::WARN
|
38
|
+
log
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
require 'em-hiredis/event_emitter'
|
45
|
+
require 'em-hiredis/connection'
|
46
|
+
require 'em-hiredis/base_client'
|
47
|
+
require 'em-hiredis/client'
|
48
|
+
require 'em-hiredis/pubsub_client'
|
49
|
+
require 'em-hiredis/lock'
|
@@ -0,0 +1,199 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module EventMachine::Hiredis
|
4
|
+
# Emits the following events
|
5
|
+
#
|
6
|
+
# * :connected - on successful connection or reconnection
|
7
|
+
# * :reconnected - on successful reconnection
|
8
|
+
# * :disconnected - no longer connected, when previously in connected state
|
9
|
+
# * :reconnect_failed(failure_number) - a reconnect attempt failed
|
10
|
+
# This event is passed number of failures so far (1,2,3...)
|
11
|
+
# * :monitor
|
12
|
+
#
|
13
|
+
class BaseClient
|
14
|
+
include EventEmitter
|
15
|
+
include EM::Deferrable
|
16
|
+
|
17
|
+
attr_reader :host, :port, :password, :db
|
18
|
+
|
19
|
+
def initialize(host='localhost', port='6379', password=nil, db=nil)
|
20
|
+
@host, @port, @password, @db = host, port, password, db
|
21
|
+
@defs = []
|
22
|
+
@command_queue = []
|
23
|
+
|
24
|
+
@closing_connection = false
|
25
|
+
@reconnect_failed_count = 0
|
26
|
+
@reconnect_timer = nil
|
27
|
+
@failed = false
|
28
|
+
|
29
|
+
self.on(:failed) {
|
30
|
+
@failed = true
|
31
|
+
@command_queue.each do |df, _, _|
|
32
|
+
df.fail(Error.new("Redis connection in failed state"))
|
33
|
+
end
|
34
|
+
@command_queue = []
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
# Configure the redis connection to use
|
39
|
+
#
|
40
|
+
# In usual operation, the uri should be passed to initialize. This method
|
41
|
+
# is useful for example when failing over to a slave connection at runtime
|
42
|
+
#
|
43
|
+
def configure(uri_string)
|
44
|
+
uri = URI(uri_string)
|
45
|
+
@host = uri.host
|
46
|
+
@port = uri.port
|
47
|
+
@password = uri.password
|
48
|
+
path = uri.path[1..-1]
|
49
|
+
@db = path.empty? ? nil : path
|
50
|
+
end
|
51
|
+
|
52
|
+
def connect
|
53
|
+
@connection = EM.connect(@host, @port, Connection, @host, @port)
|
54
|
+
|
55
|
+
@connection.on(:closed) do
|
56
|
+
if @connected
|
57
|
+
@defs.each { |d| d.fail(Error.new("Redis disconnected")) }
|
58
|
+
@defs = []
|
59
|
+
@deferred_status = nil
|
60
|
+
@connected = false
|
61
|
+
unless @closing_connection
|
62
|
+
reconnect
|
63
|
+
end
|
64
|
+
emit(:disconnected)
|
65
|
+
EM::Hiredis.logger.info("#{@connection.to_s} disconnected")
|
66
|
+
else
|
67
|
+
unless @closing_connection
|
68
|
+
@reconnect_failed_count += 1
|
69
|
+
@reconnect_timer = EM.add_timer(EM::Hiredis.reconnect_timeout) {
|
70
|
+
@reconnect_timer = nil
|
71
|
+
reconnect
|
72
|
+
}
|
73
|
+
emit(:reconnect_failed, @reconnect_failed_count)
|
74
|
+
EM::Hiredis.logger.info("#{@connection.to_s} reconnect failed")
|
75
|
+
|
76
|
+
if @reconnect_failed_count >= 4
|
77
|
+
emit(:failed)
|
78
|
+
self.fail(Error.new("Could not connect after 4 attempts"))
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
@connection.on(:connected) do
|
85
|
+
@connected = true
|
86
|
+
@reconnect_failed_count = 0
|
87
|
+
@failed = false
|
88
|
+
|
89
|
+
select(@db) if @db
|
90
|
+
auth(@password) if @password
|
91
|
+
|
92
|
+
@command_queue.each do |df, command, args|
|
93
|
+
@connection.send_command(command, *args)
|
94
|
+
@defs.push(df)
|
95
|
+
end
|
96
|
+
@command_queue = []
|
97
|
+
|
98
|
+
emit(:connected)
|
99
|
+
EM::Hiredis.logger.info("#{@connection.to_s} connected")
|
100
|
+
succeed
|
101
|
+
|
102
|
+
if @reconnecting
|
103
|
+
@reconnecting = false
|
104
|
+
emit(:reconnected)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
@connection.on(:message) do |reply|
|
109
|
+
if RuntimeError === reply
|
110
|
+
raise "Replies out of sync: #{reply.inspect}" if @defs.empty?
|
111
|
+
deferred = @defs.shift
|
112
|
+
error = Error.new("Error reply from redis")
|
113
|
+
error.redis_error = reply
|
114
|
+
deferred.fail(error) if deferred
|
115
|
+
else
|
116
|
+
handle_reply(reply)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
@connected = false
|
121
|
+
@reconnecting = false
|
122
|
+
|
123
|
+
return self
|
124
|
+
end
|
125
|
+
|
126
|
+
# Indicates that commands have been sent to redis but a reply has not yet
|
127
|
+
# been received
|
128
|
+
#
|
129
|
+
# This can be useful for example to avoid stopping the
|
130
|
+
# eventmachine reactor while there are outstanding commands
|
131
|
+
#
|
132
|
+
def pending_commands?
|
133
|
+
@connected && @defs.size > 0
|
134
|
+
end
|
135
|
+
|
136
|
+
def connected?
|
137
|
+
@connected
|
138
|
+
end
|
139
|
+
|
140
|
+
def select(db, &blk)
|
141
|
+
@db = db
|
142
|
+
method_missing(:select, db, &blk)
|
143
|
+
end
|
144
|
+
|
145
|
+
def auth(password, &blk)
|
146
|
+
@password = password
|
147
|
+
method_missing(:auth, password, &blk)
|
148
|
+
end
|
149
|
+
|
150
|
+
def close_connection
|
151
|
+
EM.cancel_timer(@reconnect_timer) if @reconnect_timer
|
152
|
+
@closing_connection = true
|
153
|
+
@connection.close_connection_after_writing
|
154
|
+
end
|
155
|
+
|
156
|
+
def reconnect_connection
|
157
|
+
EM.cancel_timer(@reconnect_timer) if @reconnect_timer
|
158
|
+
reconnect
|
159
|
+
end
|
160
|
+
|
161
|
+
private
|
162
|
+
|
163
|
+
def method_missing(sym, *args)
|
164
|
+
deferred = EM::DefaultDeferrable.new
|
165
|
+
# Shortcut for defining the callback case with just a block
|
166
|
+
deferred.callback { |result| yield(result) } if block_given?
|
167
|
+
|
168
|
+
if @connected
|
169
|
+
@connection.send_command(sym, *args)
|
170
|
+
@defs.push(deferred)
|
171
|
+
elsif @failed
|
172
|
+
deferred.fail(Error.new("Redis connection in failed state"))
|
173
|
+
else
|
174
|
+
@command_queue << [deferred, sym, args]
|
175
|
+
end
|
176
|
+
|
177
|
+
deferred
|
178
|
+
end
|
179
|
+
|
180
|
+
def reconnect
|
181
|
+
@reconnecting = true
|
182
|
+
@connection.reconnect @host, @port
|
183
|
+
EM::Hiredis.logger.info("#{@connection.to_s} reconnecting")
|
184
|
+
end
|
185
|
+
|
186
|
+
def handle_reply(reply)
|
187
|
+
if @defs.empty?
|
188
|
+
if @monitoring
|
189
|
+
emit(:monitor, reply)
|
190
|
+
else
|
191
|
+
raise "Replies out of sync: #{reply.inspect}"
|
192
|
+
end
|
193
|
+
else
|
194
|
+
deferred = @defs.shift
|
195
|
+
deferred.succeed(reply) if deferred
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|