em-hiredis 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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