em-hiredis 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e19e2546bb52eb3e8f9f4a969d5666cd04eb0f39
4
+ data.tar.gz: 80435303e1cf652b489855c8e8f83228e614918e
5
+ SHA512:
6
+ metadata.gz: aeea4ec51cc1a5ca3f0cf1ccecc5f00513d7840f1af34460ee5c19768e14d832d3460ada718927ac92c467d95a68f9e5f170bb318ea2b19510ef9b9c01de8158
7
+ data.tar.gz: 15d493eb81dfe7d044c8a55d25b3b269dd2af36d9d129fd5bfbd9d598f30d828a8ca7643a7a7f82a8f264b21d4ca8a2d5f5e43f25c48029792ff8024ba1a8062
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 (2013-04-05)
4
+
5
+ [NEW] Richer interface for pubsub (accessible via `client.pubsub`). See example in `examples/pubsub.rb`.
6
+
7
+ [NEW] Better failure handling:
8
+
9
+ * Clients now emit the following events: connected, reconnected, disconnected, reconnect_failed (passes the number of consecutive failures)
10
+ * Client is considered failed after 4 consecutive failures
11
+ * Fails all queued commands when client failed
12
+ * Can now reconfiure and reconnect an exising client
13
+ * Reconnect timeout can be configured (defaults to 0.5s)
14
+
15
+ [NEW] Added `EM::Hiredis::Lock` and `EM::Hiredis::PersistentLock`
16
+
17
+ [CHANGE] When a redis command fails, the errback is now always passed an `EM::Hiredis::Error`.
18
+
19
+ [FIX] Fixed info parsing for Redis 2.6
data/README.md CHANGED
@@ -1,5 +1,20 @@
1
- Getting started
2
- ===============
1
+ # em-hiredis
2
+
3
+ ## What
4
+
5
+ A Redis client for EventMachine designed to be fast and simple.
6
+
7
+ ## Why
8
+
9
+ I wanted a client which:
10
+
11
+ * used the C hiredis library to parse redis replies
12
+ * had a convenient API for pubsub
13
+ * exposed the state of the underlying redis connections so that custom failover logic could be written outside the library
14
+
15
+ Also, <https://github.com/madsimian/em-redis> is no longer maintained.
16
+
17
+ ## Getting started
3
18
 
4
19
  Connect to redis:
5
20
 
@@ -10,11 +25,9 @@ Or, connect to redis with a redis URL (for a different host, port, password, DB)
10
25
 
11
26
  redis = EM::Hiredis.connect("redis://:secretpassword@example.com:9000/4")
12
27
 
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" }
28
+ Commands may be sent immediately. Any commands sent while connecting to redis will be queued.
16
29
 
17
- All redis commands are available without any remapping of names
30
+ All redis commands are available without any remapping of names, and return a deferrable
18
31
 
19
32
  redis.set('foo', 'bar').callback {
20
33
  redis.get('foo').callback { |value|
@@ -22,40 +35,54 @@ All redis commands are available without any remapping of names
22
35
  }
23
36
  }
24
37
 
38
+ If redis replies with an error (for example you called a hash operation against a set or the database is full), or if the redis connection disconnects before the command returns, the deferrable will fail.
39
+
40
+ redis.sadd('aset', 'member').callback {
41
+ response_deferrable = redis.hget('aset', 'member')
42
+ response_deferrable.errback { |e|
43
+ p e # => #<EventMachine::Hiredis::RedisError: Error reply from redis (wrapped in redis_error)>
44
+ p e.redis_error # => #<RuntimeError: ERR Operation against a key holding the wrong kind of value>
45
+ }
46
+ }
47
+
25
48
  As a shortcut, if you're only interested in binding to the success case you can simply provide a block to any command
26
49
 
27
50
  redis.get('foo') { |value|
28
51
  p [:returned, value]
29
52
  }
30
53
 
31
- Handling failure
32
- ----------------
54
+ ## Understanding the state of the connection
33
55
 
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:
56
+ When a connection to redis server closes, a `:disconnected` event will be emitted and the connection will be immediately reconnect. If the connection reconnects a `:connected` event will be emitted.
35
57
 
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
- }
58
+ If a reconnect fails to connect, a `:reconnect_failed` event will be emitted (rather than `:disconnected`) with the number of consecutive failures, and the connection will be retried after a timeout (defaults to 0.5s, can be set via `EM::Hiredis.reconnect_timeout=`).
42
59
 
43
- Pubsub
44
- ------
60
+ If a client fails to reconnect 4 consecutive times then a `:failed` event will be emitted, and any queued redis commands will be failed (otherwise they would be queued forever waiting for a reconnect).
45
61
 
46
- This example should explain things. Once a redis connection is in a pubsub state, you must make sure you only send pubsub commands.
62
+ ## Pubsub
47
63
 
64
+ The way pubsub works in redis is that once a subscribe has been made on a connection, it's only possible to send (p)subscribe or (p)unsubscribe commands on that connection. The connection will also receive messages which are not replies to commands.
65
+
66
+ The regular `EM::Hiredis::Client` no longer understands pubsub messages - this logic has been moved to `EM::Hiredis::PubsubClient`. The pubsub client can either be initialized directly (see code) or you can get one connected to the same redis server by calling `#pubsub` on an existing `EM::Hiredis::Client` instance.
67
+
68
+ Pubsub can either be used in em-hiredis in a close-to-the-metal fashion, or you can use the convenience functionality for binding blocks to subscriptions if you prefer (recommended).
69
+
70
+ ### Close to the metal
71
+
72
+ Basically just bind to `:message` and `:pmessage` events:
73
+
74
+ # Create two connections, one will be used for subscribing
48
75
  redis = EM::Hiredis.connect
49
- subscriber = EM::Hiredis.connect
76
+ pubsub = redis.pubsub
50
77
 
51
- subscriber.subscribe('bar.0')
52
- subscriber.psubscribe('bar.*')
78
+ pubsub.subscribe('bar.0').callback { puts "Subscribed" }
79
+ pubsub.psubscribe('bar.*')
53
80
 
54
- subscriber.on(:message) { |channel, message|
81
+ pubsub.on(:message) { |channel, message|
55
82
  p [:message, channel, message]
56
83
  }
57
84
 
58
- subscriber.on(:pmessage) { |key, channel, message|
85
+ pubsub.on(:pmessage) { |key, channel, message|
59
86
  p [:pmessage, key, channel, message]
60
87
  }
61
88
 
@@ -65,17 +92,43 @@ This example should explain things. Once a redis connection is in a pubsub state
65
92
  }
66
93
  }
67
94
 
68
- Hacking
69
- -------
95
+ ### Richer interface to pubsub
96
+
97
+ If you pass a block to `subscribe` or `psubscribe`, the passed block will be called whenever a message arrives on that subscription:
98
+
99
+ redis = EM::Hiredis.connect
100
+
101
+ puts "Subscribing"
102
+ redis.pubsub.subscribe("foo") { |msg|
103
+ p [:sub1, msg]
104
+ }
105
+
106
+ redis.pubsub.psubscribe("f*") { |msg|
107
+ p [:sub2, msg]
108
+ }
109
+
110
+ EM.add_periodic_timer(1) {
111
+ redis.publish("foo", "Hello")
112
+ }
113
+
114
+ EM.add_timer(5) {
115
+ puts "Unsubscribing sub1"
116
+ redis.pubsub.unsubscribe("foo")
117
+ }
118
+
119
+ It's possible to subscribe to the same channel multiple time and just unsubscribe a single callback using `unsubscribe_proc` or `punsubscribe_proc`.
120
+
121
+ ## Developing
70
122
 
71
123
  Hacking on em-hiredis is pretty simple, make sure you have Bundler installed:
72
124
 
73
125
  gem install bundler
74
126
  bundle
75
127
 
76
- To run all the tests:
128
+ In order to run the tests you need to have a local redis server running on port 6379. Run all the tests:
77
129
 
78
- bundle exec rake
130
+ # WARNING: The tests call flushdb on db 9 - this clears all keys!
131
+ bundle exec rake
79
132
 
80
133
  To run an individual test:
81
134
 
@@ -0,0 +1,14 @@
1
+ $:.unshift(File.expand_path('../../lib', __FILE__))
2
+ require 'em-hiredis'
3
+
4
+ EM.run {
5
+ redis = EM::Hiredis.connect
6
+
7
+ redis.sadd('aset', 'member').callback {
8
+ response_deferrable = redis.hget('aset', 'member')
9
+ response_deferrable.errback { |e|
10
+ p e # => #<RuntimeError: ERR Operation against a key holding the wrong kind of value>
11
+ p e.redis_error
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,24 @@
1
+ $:.unshift(File.expand_path('../../lib', __FILE__))
2
+ require 'em-hiredis'
3
+
4
+ EM.run {
5
+ redis = EM::Hiredis.connect
6
+
7
+ puts "Subscribing"
8
+ redis.pubsub.subscribe("foo") { |msg|
9
+ p [:sub1, msg]
10
+ }
11
+
12
+ redis.pubsub.psubscribe("f*") { |msg|
13
+ p [:sub2, msg]
14
+ }
15
+
16
+ EM.add_periodic_timer(1) {
17
+ redis.publish("foo", "Hello")
18
+ }
19
+
20
+ EM.add_timer(5) {
21
+ puts "Unsubscribing sub1"
22
+ redis.pubsub.unsubscribe("foo")
23
+ }
24
+ }
@@ -0,0 +1,51 @@
1
+ $:.unshift(File.expand_path('../../lib', __FILE__))
2
+ require 'em-hiredis'
3
+
4
+ EM.run {
5
+ redis = EM::Hiredis.connect
6
+
7
+ # If you pass a block to subscribe it will be called whenever a message
8
+ # is received on this channel
9
+ redis.pubsub.subscribe('foo') { |message|
10
+ puts "Block received #{message}"
11
+ }
12
+
13
+ # You can also pass any other object which responds to call if you wish
14
+ callback = Proc.new { |message|
15
+ "Proc received #{message}"
16
+ }
17
+ df = redis.pubsub.subscribe('foo', callback)
18
+
19
+ # All calls return a deferrable
20
+ df.callback { |reply|
21
+ p [:subscription_succeeded, reply]
22
+ }
23
+
24
+ # Passing such an object is useful if you want to unsubscribe
25
+ redis.pubsub.unsubscribe_proc('foo', callback)
26
+
27
+ # Or if you want to call a method on a certain object
28
+ class Thing
29
+ def receive_message(message)
30
+ puts "Thing received #{message}"
31
+ end
32
+ end
33
+ redis.pubsub.subscribe('bar', Thing.new.method(:receive_message))
34
+
35
+ # You can also get all the following raw events:
36
+ # message pmessage subscribe unsubscribe psubscribe punsubscribe
37
+ redis.pubsub.on(:message) { |channel, message|
38
+ p [:message_received, channel, message]
39
+ }
40
+ redis.pubsub.on(:unsubscribe) { |channel, remaining_subscriptions|
41
+ p [:unsubscribe_happened, channel, remaining_subscriptions]
42
+ }
43
+
44
+ EM.add_timer(1) {
45
+ # You can also unsubscribe completely from a channel
46
+ redis.pubsub.unsubscribe('foo')
47
+
48
+ # Publishing events
49
+ redis.publish('bar', 'Hello')
50
+ }
51
+ }
@@ -0,0 +1,25 @@
1
+ $:.unshift(File.expand_path('../../lib', __FILE__))
2
+ require 'em-hiredis'
3
+
4
+ EM.run {
5
+ # Create two connections, one will be used for subscribing
6
+ redis = EM::Hiredis.connect
7
+ pubsub = redis.pubsub
8
+
9
+ pubsub.subscribe('bar.0').callback { puts "Subscribed" }
10
+ pubsub.psubscribe('bar.*')
11
+
12
+ pubsub.on(:message) { |channel, message|
13
+ p [:message, channel, message]
14
+ }
15
+
16
+ pubsub.on(:pmessage) { |key, channel, message|
17
+ p [:pmessage, key, channel, message]
18
+ }
19
+
20
+ EM.add_periodic_timer(1) {
21
+ redis.publish("bar.#{rand(2)}", "hello").errback { |e|
22
+ p [:publisherror, e]
23
+ }
24
+ }
25
+ }
@@ -1,11 +1,26 @@
1
1
  require 'eventmachine'
2
- require 'uri'
3
2
 
4
3
  module EventMachine
5
4
  module Hiredis
5
+ # All em-hiredis errors should descend from EM::Hiredis::Error
6
+ class Error < RuntimeError; end
7
+
8
+ # In the case of error responses from Redis, the RuntimeError returned
9
+ # by ::Hiredis will be wrapped
10
+ class RedisError < Error
11
+ attr_accessor :redis_error
12
+ end
13
+
14
+ class << self
15
+ attr_accessor :reconnect_timeout
16
+ end
17
+ self.reconnect_timeout = 0.5
18
+
6
19
  def self.setup(uri = nil)
7
- url = URI(uri || ENV["REDIS_URL"] || "redis://127.0.0.1:6379/0")
8
- Client.new(url.host, url.port, url.password, url.path[1..-1])
20
+ uri = uri || ENV["REDIS_URL"] || "redis://127.0.0.1:6379/0"
21
+ client = Client.new
22
+ client.configure(uri)
23
+ client
9
24
  end
10
25
 
11
26
  def self.connect(uri = nil)
@@ -26,9 +41,14 @@ module EventMachine
26
41
  log
27
42
  end
28
43
  end
44
+
45
+ autoload :Lock, 'em-hiredis/lock'
46
+ autoload :PersistentLock, 'em-hiredis/persistent_lock'
29
47
  end
30
48
  end
31
49
 
32
50
  require 'em-hiredis/event_emitter'
33
51
  require 'em-hiredis/connection'
52
+ require 'em-hiredis/base_client'
34
53
  require 'em-hiredis/client'
54
+ require 'em-hiredis/pubsub_client'
@@ -0,0 +1,200 @@
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.to_i # Empty path => 0
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
+ # Next tick avoids reconnecting after for example EM.stop
63
+ EM.next_tick { reconnect }
64
+ end
65
+ emit(:disconnected)
66
+ EM::Hiredis.logger.info("#{@connection} Disconnected")
67
+ else
68
+ unless @closing_connection
69
+ @reconnect_failed_count += 1
70
+ @reconnect_timer = EM.add_timer(EM::Hiredis.reconnect_timeout) {
71
+ @reconnect_timer = nil
72
+ reconnect
73
+ }
74
+ emit(:reconnect_failed, @reconnect_failed_count)
75
+ EM::Hiredis.logger.info("#{@connection} Reconnect failed")
76
+
77
+ if @reconnect_failed_count >= 4
78
+ emit(:failed)
79
+ self.fail(Error.new("Could not connect after 4 attempts"))
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ @connection.on(:connected) do
86
+ @connected = true
87
+ @reconnect_failed_count = 0
88
+ @failed = false
89
+
90
+ select(@db) unless @db == 0
91
+ auth(@password) if @password
92
+
93
+ @command_queue.each do |df, command, args|
94
+ @connection.send_command(command, args)
95
+ @defs.push(df)
96
+ end
97
+ @command_queue = []
98
+
99
+ emit(:connected)
100
+ EM::Hiredis.logger.info("#{@connection} Connected")
101
+ succeed
102
+
103
+ if @reconnecting
104
+ @reconnecting = false
105
+ emit(:reconnected)
106
+ end
107
+ end
108
+
109
+ @connection.on(:message) do |reply|
110
+ if RuntimeError === reply
111
+ raise "Replies out of sync: #{reply.inspect}" if @defs.empty?
112
+ deferred = @defs.shift
113
+ error = RedisError.new("Error reply from redis (wrapped in redis_error)")
114
+ error.redis_error = reply
115
+ deferred.fail(error) if deferred
116
+ else
117
+ handle_reply(reply)
118
+ end
119
+ end
120
+
121
+ @connected = false
122
+ @reconnecting = false
123
+
124
+ return self
125
+ end
126
+
127
+ # Indicates that commands have been sent to redis but a reply has not yet
128
+ # been received
129
+ #
130
+ # This can be useful for example to avoid stopping the
131
+ # eventmachine reactor while there are outstanding commands
132
+ #
133
+ def pending_commands?
134
+ @connected && @defs.size > 0
135
+ end
136
+
137
+ def connected?
138
+ @connected
139
+ end
140
+
141
+ def select(db, &blk)
142
+ @db = db
143
+ method_missing(:select, db, &blk)
144
+ end
145
+
146
+ def auth(password, &blk)
147
+ @password = password
148
+ method_missing(:auth, password, &blk)
149
+ end
150
+
151
+ def close_connection
152
+ EM.cancel_timer(@reconnect_timer) if @reconnect_timer
153
+ @closing_connection = true
154
+ @connection.close_connection_after_writing
155
+ end
156
+
157
+ def reconnect_connection
158
+ EM.cancel_timer(@reconnect_timer) if @reconnect_timer
159
+ reconnect
160
+ end
161
+
162
+ private
163
+
164
+ def method_missing(sym, *args)
165
+ deferred = EM::DefaultDeferrable.new
166
+ # Shortcut for defining the callback case with just a block
167
+ deferred.callback { |result| yield(result) } if block_given?
168
+
169
+ if @connected
170
+ @connection.send_command(sym, args)
171
+ @defs.push(deferred)
172
+ elsif @failed
173
+ deferred.fail(Error.new("Redis connection in failed state"))
174
+ else
175
+ @command_queue << [deferred, sym, args]
176
+ end
177
+
178
+ deferred
179
+ end
180
+
181
+ def reconnect
182
+ @reconnecting = true
183
+ @connection.reconnect @host, @port
184
+ EM::Hiredis.logger.info("#{@connection} Reconnecting")
185
+ end
186
+
187
+ def handle_reply(reply)
188
+ if @defs.empty?
189
+ if @monitoring
190
+ emit(:monitor, reply)
191
+ else
192
+ raise "Replies out of sync: #{reply.inspect}"
193
+ end
194
+ else
195
+ deferred = @defs.shift
196
+ deferred.succeed(reply) if deferred
197
+ end
198
+ end
199
+ end
200
+ end