em-hiredis 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 48fe707c4b77c8666f0466d1a33f342222f01341
4
- data.tar.gz: f9119b5ab7ad4af24a98f8392556f85813c1baf0
3
+ metadata.gz: d9403dec51c7c192ce9f40a3850f1bec4fc8adb6
4
+ data.tar.gz: bec13c8424a5ac75854dceb0d17144ac16c65b93
5
5
  SHA512:
6
- metadata.gz: 0039ac23a12e4027902ea932b5f518be4a4ddfcf553a7cdb30103a46d465f35b2954be053135e26d90eea4c2d2f922ff550925ce89a14191a0d800d1b05b41ca
7
- data.tar.gz: 3c5a950d9abbde1c1b1ef55a359f7804d9128b9f6527a43b5126423fc70d7c0a809fedd7dbe482a430db8f177cf2eb1f25c4013414b3fd78ee99c3c6b22d24a5
6
+ metadata.gz: 632d2d5f5d0495f52d7638622fa2fc5ea4149872100da8eefbcaee6d6655fd5de6e4d5bed0b4dd2c441b357e648a1646ab65c858770b7dd1c8370fa517358b17
7
+ data.tar.gz: c4696c3fd994ff092efd11f35da929d5b33704cd7ba6d67c0b470f7e458bac9d80720d8e15ec12816cfc687db10081136fae08d35133cbe965d13ce7bd56be34
data/README.md CHANGED
@@ -103,7 +103,7 @@ If you pass a block to `subscribe` or `psubscribe`, the passed block will be cal
103
103
  p [:sub1, msg]
104
104
  }
105
105
 
106
- redis.pubsub.psubscribe("f*") { |msg|
106
+ redis.pubsub.psubscribe("f*") { |channel, msg|
107
107
  p [:sub2, msg]
108
108
  }
109
109
 
@@ -147,6 +147,23 @@ As a final convenience, it is possible to load all lua scripts from a directory
147
147
 
148
148
  For examples see `examples/lua.rb` or `lib/em-hiredis/lock_lua`.
149
149
 
150
+ ## Inactivity checks
151
+
152
+ Sometimes a network connection may hang in ways which are difficult to detect or involve very long timeouts before they can be detected from the application layer. This is especially true of Redis Pubsub connections, as they are not request-response driven. It is very difficult for a listening client to descern between a hung connection and a server with nothing to say.
153
+
154
+ To start an application layer ping-pong mechanism for testing connection liveness, call the following at any time on a client:
155
+
156
+ redis.configure_inactivity_check(5, 3)
157
+
158
+ This configures a `PING` command to be sent if 5 seconds elapse without receiving any data from the server, and a reconnection to be triggered if a futher 3 seconds elapse after the `PING` is submitted.
159
+
160
+ This configuration is per client, you may choose different value for clients with different expected traffic patterns, or activate it on some and not at all on others.
161
+
162
+ ### PING and Pubsub
163
+
164
+ Because the Redis Pubsub protocol limits the set of valid commands on a connection once it is in "Pubsub" mode, `PING` is not supported in this case (though it may be in future, see https://github.com/antirez/redis/issues/420). In order to create some valid request-response traffic on the connection, a Pubsub connection will issue `SUBSCRIBE "__em-hiredis-ping"`, followed by a corresponding `UNSUBSCRIBE` immediately on success of the subscribe.
165
+ While less than ideal, this is the case where an application layer inactivity check is most valuable, and so the trade off is reasonable until `PING` is supported correctly on Pubsub connections.
166
+
150
167
  ## Developing
151
168
 
152
169
  You need bundler and a local redis server running on port 6379 to run the test suite.
@@ -13,7 +13,7 @@ Gem::Specification.new do |s|
13
13
  s.description = %q{Eventmachine redis client using hiredis native parser}
14
14
 
15
15
  s.add_dependency 'eventmachine', '~> 1.0'
16
- s.add_dependency 'hiredis', '~> 0.5.0'
16
+ s.add_dependency 'hiredis', '~> 0.6.0'
17
17
 
18
18
  s.add_development_dependency 'em-spec', '~> 0.2.5'
19
19
  s.add_development_dependency 'rspec', '~> 2.6.0'
@@ -25,6 +25,8 @@ module EventMachine::Hiredis
25
25
  @reconnect_timer = nil
26
26
  @failed = false
27
27
 
28
+ @inactive_seconds = 0
29
+
28
30
  self.on(:failed) {
29
31
  @failed = true
30
32
  @command_queue.each do |df, _, _|
@@ -72,6 +74,7 @@ module EventMachine::Hiredis
72
74
  @connection = EM.connect(@host, @port, Connection, @host, @port)
73
75
 
74
76
  @connection.on(:closed) do
77
+ cancel_inactivity_checks
75
78
  if @connected
76
79
  @defs.each { |d| d.fail(Error.new("Redis disconnected")) }
77
80
  @defs = []
@@ -106,8 +109,8 @@ module EventMachine::Hiredis
106
109
  @reconnect_failed_count = 0
107
110
  @failed = false
108
111
 
109
- select(@db) unless @db == 0
110
112
  auth(@password) if @password
113
+ select(@db) unless @db == 0
111
114
 
112
115
  @command_queue.each do |df, command, args|
113
116
  @connection.send_command(command, args)
@@ -115,6 +118,8 @@ module EventMachine::Hiredis
115
118
  end
116
119
  @command_queue = []
117
120
 
121
+ schedule_inactivity_checks
122
+
118
123
  emit(:connected)
119
124
  EM::Hiredis.logger.info("#{@connection} Connected")
120
125
  succeed
@@ -133,6 +138,7 @@ module EventMachine::Hiredis
133
138
  error.redis_error = reply
134
139
  deferred.fail(error) if deferred
135
140
  else
141
+ @inactive_seconds = 0
136
142
  handle_reply(reply)
137
143
  end
138
144
  end
@@ -181,6 +187,21 @@ module EventMachine::Hiredis
181
187
  reconnect
182
188
  end
183
189
 
190
+ # Starts an inactivity checker which will ping redis if nothing has been
191
+ # heard on the connection for `trigger_secs` seconds and forces a reconnect
192
+ # after a further `response_timeout` seconds if we still don't hear anything.
193
+ def configure_inactivity_check(trigger_secs, response_timeout)
194
+ raise ArgumentError('trigger_secs must be > 0') unless trigger_secs.to_i > 0
195
+ raise ArgumentError('response_timeout must be > 0') unless response_timeout.to_i > 0
196
+
197
+ @inactivity_trigger_secs = trigger_secs.to_i
198
+ @inactivity_response_timeout = response_timeout.to_i
199
+
200
+ # Start the inactivity check now only if we're already conected, otherwise
201
+ # the connected event will schedule it.
202
+ schedule_inactivity_checks if @connected
203
+ end
204
+
184
205
  private
185
206
 
186
207
  def method_missing(sym, *args)
@@ -206,6 +227,27 @@ module EventMachine::Hiredis
206
227
  EM::Hiredis.logger.info("#{@connection} Reconnecting")
207
228
  end
208
229
 
230
+ def cancel_inactivity_checks
231
+ EM.cancel_timer(@inactivity_timer) if @inactivity_timer
232
+ @inactivity_timer = nil
233
+ end
234
+
235
+ def schedule_inactivity_checks
236
+ if @inactivity_trigger_secs
237
+ @inactive_seconds = 0
238
+ @inactivity_timer = EM.add_periodic_timer(1) {
239
+ @inactive_seconds += 1
240
+ if @inactive_seconds > @inactivity_trigger_secs + @inactivity_response_timeout
241
+ EM::Hiredis.logger.error "#{@connection} No response to ping, triggering reconnect"
242
+ reconnect!
243
+ elsif @inactive_seconds > @inactivity_trigger_secs
244
+ EM::Hiredis.logger.debug "#{@connection} Connection inactive, triggering ping"
245
+ ping
246
+ end
247
+ }
248
+ end
249
+ end
250
+
209
251
  def handle_reply(reply)
210
252
  if @defs.empty?
211
253
  if @monitoring
@@ -2,6 +2,8 @@ module EventMachine::Hiredis
2
2
  class PubsubClient < BaseClient
3
3
  PUBSUB_MESSAGES = %w{message pmessage subscribe unsubscribe psubscribe punsubscribe}.freeze
4
4
 
5
+ PING_CHANNEL = '__em-hiredis-ping'
6
+
5
7
  def initialize(host='localhost', port='6379', password=nil, db=nil)
6
8
  @subs, @psubs = [], []
7
9
  @pubsub_defs = Hash.new { |h,k| h[k] = [] }
@@ -125,7 +127,20 @@ module EventMachine::Hiredis
125
127
  end
126
128
  return df
127
129
  end
128
-
130
+
131
+ # Pubsub connections to not support even the PING command, but it is useful,
132
+ # especially with read-only connections like pubsub, to be able to check that
133
+ # the TCP connection is still usefully alive.
134
+ #
135
+ # This is not particularly elegant, but it's probably the best we can do
136
+ # for now. Ping support for pubsub connections is being considerred:
137
+ # https://github.com/antirez/redis/issues/420
138
+ def ping
139
+ subscribe(PING_CHANNEL).callback {
140
+ unsubscribe(PING_CHANNEL)
141
+ }
142
+ end
143
+
129
144
  private
130
145
 
131
146
  # Send a command to redis without adding a deferrable for it. This is
@@ -1,5 +1,5 @@
1
1
  module EventMachine
2
2
  module Hiredis
3
- VERSION = "0.3.0"
3
+ VERSION = "0.3.1"
4
4
  end
5
5
  end
@@ -0,0 +1,66 @@
1
+ require 'spec_helper'
2
+ require 'support/inprocess_redis_mock'
3
+
4
+ def connect_mock(timeout = 10, url = "redis://localhost:6381", server = nil, &blk)
5
+ em(timeout) do
6
+ IRedisMock.start
7
+ redis = EventMachine::Hiredis.connect(url)
8
+ blk.call(redis)
9
+ IRedisMock.stop
10
+ end
11
+ end
12
+
13
+ describe EM::Hiredis::BaseClient do
14
+ it "should ping after activity timeout reached" do
15
+ connect_mock do |redis|
16
+ redis.configure_inactivity_check(2, 1)
17
+ EM.add_timer(4) {
18
+ IRedisMock.received.should include("ping")
19
+ done
20
+ }
21
+ end
22
+ end
23
+
24
+ it "should not ping before activity timeout reached" do
25
+ connect_mock do |redis|
26
+ redis.configure_inactivity_check(3, 1)
27
+ EM.add_timer(2) {
28
+ IRedisMock.received.should_not include("ping")
29
+ done
30
+ }
31
+ end
32
+ end
33
+
34
+ it "should ping after timeout reached even though command has been sent (no response)" do
35
+ connect_mock do |redis|
36
+ redis.configure_inactivity_check(2, 1)
37
+ IRedisMock.pause # no responses from now on
38
+
39
+ EM.add_timer(1.5) {
40
+ redis.get "test"
41
+ }
42
+
43
+ EM.add_timer(4) {
44
+ IRedisMock.received.should include("ping")
45
+ done
46
+ }
47
+ end
48
+ end
49
+
50
+ it "should trigger a reconnect when there's no response to ping" do
51
+ connect_mock do |redis|
52
+ redis.configure_inactivity_check(2, 1)
53
+ IRedisMock.pause # no responses from now on
54
+
55
+ EM.add_timer(1.5) {
56
+ redis.get "test"
57
+ }
58
+
59
+ EM.add_timer(5) {
60
+ IRedisMock.received.should include("disconnect")
61
+ done
62
+ }
63
+ end
64
+ end
65
+
66
+ end
@@ -0,0 +1,83 @@
1
+ module IRedisMock
2
+ def self.start(replies = {})
3
+ @sig = EventMachine::start_server("127.0.0.1", 6381, Connection)
4
+ @received = []
5
+ @replies = replies
6
+ @paused = false
7
+ end
8
+
9
+ def self.stop
10
+ EventMachine::stop_server(@sig)
11
+ end
12
+
13
+ def self.received
14
+ @received ||= []
15
+ end
16
+
17
+ def self.pause
18
+ @paused = true
19
+ end
20
+ def self.unpause
21
+ @paused = false
22
+ end
23
+
24
+ def self.paused
25
+ @paused
26
+ end
27
+
28
+ def self.replies
29
+ @replies
30
+ end
31
+
32
+ class Connection < EventMachine::Connection
33
+ def initialize
34
+ @data = ""
35
+ @parts = []
36
+ end
37
+
38
+ def unbind
39
+ IRedisMock.received << 'disconnect'
40
+ end
41
+
42
+ def receive_data(data)
43
+ @data << data
44
+
45
+ while (idx = @data.index("\r\n"))
46
+ @parts << @data[0..idx-1]
47
+ @data = @data[idx+2..-1]
48
+ end
49
+
50
+ while @parts.length > 0
51
+ throw "commands out of sync" unless @parts[0][0] == '*'
52
+
53
+ num_parts = @parts[0][1..-1].to_i * 2 + 1
54
+ return if @parts.length < num_parts
55
+
56
+ command_parts = @parts[0..num_parts]
57
+ @parts = @parts[num_parts..-1]
58
+
59
+ # Discard length declarations
60
+ command_line =
61
+ command_parts
62
+ .reject { |p| p[0] == '*' || p[0] == '$' }
63
+ .join ' '
64
+
65
+ if IRedisMock.replies.member?(command_line)
66
+ reply = IRedisMock.replies[command_line]
67
+ else
68
+ reply = "+OK"
69
+ end
70
+
71
+ # p "[#{command_line}] => [#{reply}]"
72
+
73
+ IRedisMock.received << command_line
74
+
75
+ if IRedisMock.paused
76
+ # puts "Paused, therefore not sending [#{reply}]"
77
+ else
78
+ send_data "#{reply}\r\n"
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: em-hiredis
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Martyn Loughran
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-06-30 00:00:00.000000000 Z
11
+ date: 2016-01-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: eventmachine
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 0.5.0
33
+ version: 0.6.0
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 0.5.0
40
+ version: 0.6.0
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: em-spec
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -114,12 +114,14 @@ files:
114
114
  - lib/em-hiredis/version.rb
115
115
  - spec/base_client_spec.rb
116
116
  - spec/connection_spec.rb
117
+ - spec/inactivity_check_spec.rb
117
118
  - spec/live_redis_protocol_spec.rb
118
119
  - spec/lock_spec.rb
119
120
  - spec/pubsub_spec.rb
120
121
  - spec/redis_commands_spec.rb
121
122
  - spec/spec_helper.rb
122
123
  - spec/support/connection_helper.rb
124
+ - spec/support/inprocess_redis_mock.rb
123
125
  - spec/support/redis_mock.rb
124
126
  - spec/url_param_spec.rb
125
127
  homepage: http://github.com/mloughran/em-hiredis
@@ -141,18 +143,20 @@ required_rubygems_version: !ruby/object:Gem::Requirement
141
143
  version: '0'
142
144
  requirements: []
143
145
  rubyforge_project: em-hiredis
144
- rubygems_version: 2.2.2
146
+ rubygems_version: 2.5.1
145
147
  signing_key:
146
148
  specification_version: 4
147
149
  summary: Eventmachine redis client
148
150
  test_files:
149
151
  - spec/base_client_spec.rb
150
152
  - spec/connection_spec.rb
153
+ - spec/inactivity_check_spec.rb
151
154
  - spec/live_redis_protocol_spec.rb
152
155
  - spec/lock_spec.rb
153
156
  - spec/pubsub_spec.rb
154
157
  - spec/redis_commands_spec.rb
155
158
  - spec/spec_helper.rb
156
159
  - spec/support/connection_helper.rb
160
+ - spec/support/inprocess_redis_mock.rb
157
161
  - spec/support/redis_mock.rb
158
162
  - spec/url_param_spec.rb