sensu-transport 2.4.0 → 3.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3df64f00afb91830bb9b3c5fe13da6a9bca16609
4
- data.tar.gz: d9819ca9cf5f0e06b4407b62fa1b8d9c51b18f58
3
+ metadata.gz: 517c32e4f19a39830cf6063c9ad922367c150606
4
+ data.tar.gz: 11096746935854892ef77ecac01e4e308e48808e
5
5
  SHA512:
6
- metadata.gz: 47d968aa20315b67bf6bb2500bc3da092e1e33546a170146e85d2cbfffc37f5262c61d767340355ded4af5255d04eea3339077ef582b1d9eb21e63afb9fad47a
7
- data.tar.gz: 5c6d272da0ccb3badc6a53f919d3c85b311a9883a7a7d2a95f05f71a01a51f2c633322a0636deff5b793258a7c78ec456cbb8a6ca5d0fae5d3247fb07c6e97f4
6
+ metadata.gz: bc8457b82d63f770d8ff504f924dbc9b624ea455420bdd5ed2cce8f666c9923ad49b4a53738869fdb331fb2be9c9569afb99fe5b44cbbbbfaad1b7609cd1fbf6
7
+ data.tar.gz: c30aefec4551ca98e5ca73ffc519e4afdaed37a91e8359691d4439c046651a75e25bb3d056d14ce46730aff30f69b6274e33833e3413647a9e60726e91036131
data/.travis.yml CHANGED
@@ -8,10 +8,11 @@ rvm:
8
8
  - jruby
9
9
  services:
10
10
  - rabbitmq
11
+ - redis
11
12
  notifications:
12
13
  irc:
13
14
  - "irc.freenode.net#sensu"
14
- script: "rspec . --tag ~ssl"
15
+ script: "bundle exec rspec . --tag ~ssl"
15
16
  addons:
16
17
  code_climate:
17
18
  repo_token: 629d024a848e2ebb1b0c0283c2046387c1b8c0d1cfd4d03289bef74a7b875d55
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
- source 'https://rubygems.org'
1
+ source "https://rubygems.org"
2
2
 
3
- # Specify your gem's dependencies in sensu-spawn.gemspec
3
+ # Specify your gem's dependencies in sensu-transport.gemspec
4
4
  gemspec
@@ -48,7 +48,9 @@ module Sensu
48
48
  def connect(options={}); end
49
49
 
50
50
  # Reconnect to the transport.
51
- def reconnect; end
51
+ #
52
+ # @param force [Boolean] the reconnect.
53
+ def reconnect(force=false); end
52
54
 
53
55
  # Indicates if connected to the transport.
54
56
  #
@@ -14,7 +14,7 @@ module Sensu
14
14
  connect_with_eligible_options
15
15
  end
16
16
 
17
- def reconnect
17
+ def reconnect(force=false)
18
18
  unless @reconnecting
19
19
  @reconnecting = true
20
20
  @before_reconnect.call
@@ -0,0 +1,311 @@
1
+ require "em-redis-unified"
2
+
3
+ require File.join(File.dirname(__FILE__), "base")
4
+
5
+ module Sensu
6
+ module Transport
7
+ class Redis < Base
8
+
9
+ # The Redis keyspace to use for the transport.
10
+ REDIS_KEYSPACE = "transport"
11
+
12
+ def initialize
13
+ @options = {}
14
+ @connections = {}
15
+ super
16
+ end
17
+
18
+ # Redis transport connection setup. This method sets `@options`
19
+ # and creates a named Redis connection "redis".
20
+ #
21
+ # @param options [Hash, String]
22
+ def connect(options={})
23
+ @options = options || {}
24
+ redis_connection("redis")
25
+ end
26
+
27
+ # Reconnect to the Redis transport. The Redis connections used
28
+ # by the transport have auto-reconnect disabled; if a single
29
+ # connection is unhealthy, all connections are closed, the
30
+ # transport is reset, and new connections are made. If the
31
+ # transport is not already reconnecting to Redis, the
32
+ # `@before_reconnect` transport callback is called.
33
+ #
34
+ # @param force [Boolean] the reconnect.
35
+ def reconnect(force=false)
36
+ @before_reconnect.call unless @reconnecting
37
+ unless @reconnecting && !force
38
+ @reconnecting = true
39
+ close
40
+ reset
41
+ connect(@options)
42
+ end
43
+ end
44
+
45
+ # Indicates if ALL Redis connections are connected.
46
+ #
47
+ # @return [TrueClass, FalseClass]
48
+ def connected?
49
+ !@connections.empty? && @connections.values.all? do |connection|
50
+ connection.connected?
51
+ end
52
+ end
53
+
54
+ # Close ALL Redis connections.
55
+ def close
56
+ @connections.each_value do |connection|
57
+ connection.close
58
+ end
59
+ end
60
+
61
+ # Publish a message to the Redis transport. The transport pipe
62
+ # type determines the method of sending messages to consumers
63
+ # using Redis, either using PubSub or a list. The appropriate
64
+ # publish method is call for the pipe type given. The Redis
65
+ # transport ignores publish options.
66
+ #
67
+ # @param type [Symbol] the transport pipe type, possible values
68
+ # are: :direct and :fanout.
69
+ # @param pipe [String] the transport pipe name.
70
+ # @param message [String] the message to be published to the transport.
71
+ # @param options [Hash] IGNORED by this transport.
72
+ # @yield [info] passes publish info to an optional callback/block.
73
+ # @yieldparam info [Hash] contains publish information, which
74
+ # may contain an error object.
75
+ def publish(type, pipe, message, options={}, &callback)
76
+ case type.to_sym
77
+ when :fanout
78
+ pubsub_publish(pipe, message, &callback)
79
+ when :direct
80
+ list_publish(pipe, message, &callback)
81
+ end
82
+ end
83
+
84
+ # Subscribe to a Redis transport pipe. The transport pipe
85
+ # type determines the method of consuming messages from Redis,
86
+ # either using PubSub or a list. The appropriate subscribe
87
+ # method is call for the pipe type given. The Redis transport
88
+ # ignores subscribe options and the funnel name.
89
+ #
90
+ # @param type [Symbol] the transport pipe type, possible values
91
+ # are: :direct and :fanout.
92
+ # @param pipe [String] the transport pipe name.
93
+ # @param funnel [String] IGNORED by this transport.
94
+ # @param options [Hash] IGNORED by this transport.
95
+ # @yield [info, message] passes message info and content to
96
+ # the consumer callback/block.
97
+ # @yieldparam info [Hash] contains message information.
98
+ # @yieldparam message [String] message.
99
+ def subscribe(type, pipe, funnel=nil, options={}, &callback)
100
+ case type.to_sym
101
+ when :fanout
102
+ pubsub_subscribe(pipe, &callback)
103
+ when :direct
104
+ list_subscribe(pipe, &callback)
105
+ end
106
+ end
107
+
108
+ # Unsubscribe from all transport pipes. This method iterates
109
+ # through the current named Redis connections, unsubscribing the
110
+ # "pubsub" connection from Redis channels, and closing/deleting
111
+ # BLPOP connections.
112
+ #
113
+ # @yield [info] passes info to an optional callback/block.
114
+ # @yieldparam info [Hash] empty hash.
115
+ def unsubscribe(&callback)
116
+ @connections.each do |name, connection|
117
+ case name
118
+ when "pubsub"
119
+ connection.unsubscribe
120
+ when /^#{REDIS_KEYSPACE}/
121
+ connection.close
122
+ @connections.delete(name)
123
+ end
124
+ end
125
+ super
126
+ end
127
+
128
+ # Redis transport pipe/funnel stats, such as message and
129
+ # consumer counts. This method is currently unable to determine
130
+ # the consumer count for a Redis list.
131
+ #
132
+ # @param funnel [String] the transport funnel to get stats for.
133
+ # @param options [Hash] IGNORED by this transport.
134
+ def stats(funnel, options={}, &callback)
135
+ redis_connection("redis").llen(funnel) do |messages|
136
+ info = {
137
+ :messages => messages,
138
+ :consumers => 0
139
+ }
140
+ callback.call(info)
141
+ end
142
+ end
143
+
144
+ private
145
+
146
+ # Reset instance variables, called when reconnecting.
147
+ def reset
148
+ @connections = {}
149
+ end
150
+
151
+ # Monitor current Redis connections, the connection "pool". A
152
+ # timer is used to check on the connections, every `3` seconds.
153
+ # If one or more connections is not connected, a forced
154
+ # `reconnect()` is triggered. If all connections are connected
155
+ # after reconnecting, the transport `@after_reconnect`
156
+ # callback is called. If a connection monitor (timer) already
157
+ # exists, it is canceled.
158
+ def monitor_connections
159
+ @connection_monitor.cancel if @connection_monitor
160
+ @connection_monitor = EM::PeriodicTimer.new(3) do
161
+ if !connected?
162
+ reconnect(true)
163
+ elsif @reconnecting
164
+ @after_reconnect.call
165
+ @reconnecting = false
166
+ end
167
+ end
168
+ end
169
+
170
+ # Return or setup a named Redis connection. This method creates
171
+ # a Redis connection object using the provided Redis transport
172
+ # options. Redis auto-reconnect is disabled as the connection
173
+ # "pool" is monitored as a whole. The transport `@on_error`
174
+ # callback is called when Redis errors are encountered. This
175
+ # method creates/replaces the connection monitor after setting
176
+ # up the connection and before adding it to the pool.
177
+ #
178
+ # @param name [String] the Redis connection name.
179
+ # @return [Object]
180
+ def redis_connection(name)
181
+ return @connections[name] if @connections[name]
182
+ connection = EM::Protocols::Redis.connect(@options)
183
+ connection.auto_reconnect = false
184
+ connection.reconnect_on_error = false
185
+ connection.on_error do |error|
186
+ @on_error.call(error)
187
+ end
188
+ monitor_connections
189
+ @connections[name] = connection
190
+ connection
191
+ end
192
+
193
+ # Create a Redis key within the defined Redis keyspace. This
194
+ # method is used to create keys that are unlikely to collide.
195
+ # The Redis connection database number is included in the Redis
196
+ # key as pubsub is not scoped to the selected database.
197
+ #
198
+ # @param type [String]
199
+ # @param name [String]
200
+ # @return [String]
201
+ def redis_key(type, name)
202
+ db = @options.is_a?(Hash) ? (@options[:db] || 0) : 0
203
+ [REDIS_KEYSPACE, db, type, name].join(":")
204
+ end
205
+
206
+ # Publish a message to a Redis channel (PubSub). The
207
+ # `redis_key()` method is used to create a Redis channel key,
208
+ # using the transport pipe name. The publish callback info
209
+ # includes the current subscriber count for the Redis channel.
210
+ #
211
+ # http://redis.io/topics/pubsub
212
+ #
213
+ # @param pipe [String] the transport pipe name.
214
+ # @param message [String] the message to be published to the transport.
215
+ # @yield [info] passes publish info to an optional callback/block.
216
+ # @yieldparam info [Hash] contains publish information.
217
+ # @yieldparam subscribers [String] current subscriber count.
218
+ def pubsub_publish(pipe, message, &callback)
219
+ channel = redis_key("channel", pipe)
220
+ redis_connection("redis").publish(channel, message) do |subscribers|
221
+ info = {:subscribers => subscribers}
222
+ callback.call(info) if callback
223
+ end
224
+ end
225
+
226
+ # Subscribe to a Redis channel (PubSub). The `redis_key()`
227
+ # method is used to create a Redis channel key, using the
228
+ # transport pipe name. The named Redis connection "pubsub" is
229
+ # used for the Redis SUBSCRIBE command set, as the Redis context
230
+ # is limited and enforced for the connection. The subscribe
231
+ # callback is called whenever a message is published to the
232
+ # Redis channel. Channel messages with the type "subscribe" and
233
+ # "unsubscribe" are ignored, only messages with type "message"
234
+ # are passsed to the provided consumer/method callback/block.
235
+ #
236
+ # http://redis.io/topics/pubsub
237
+ #
238
+ # @param pipe [String] the transport pipe name.
239
+ # @yield [info, message] passes message info and content to
240
+ # the consumer/method callback/block.
241
+ # @yieldparam info [Hash] contains the channel name.
242
+ # @yieldparam message [String] message content.
243
+ def pubsub_subscribe(pipe, &callback)
244
+ channel = redis_key("channel", pipe)
245
+ redis_connection("pubsub").subscribe(channel) do |type, channel, message|
246
+ case type
247
+ when "subscribe"
248
+ @logger.debug("subscribed to redis channel: #{channel}") if @logger
249
+ when "unsubscribe"
250
+ @logger.debug("unsubscribed from redis channel: #{channel}") if @logger
251
+ when "message"
252
+ info = {:channel => channel}
253
+ callback.call(info, message)
254
+ end
255
+ end
256
+ end
257
+
258
+ # Push (publish) a message onto a Redis list. The `redis_key()`
259
+ # method is used to create a Redis list key, using the transport
260
+ # pipe name. The publish callback info includes the current list
261
+ # size (queued).
262
+ #
263
+ # @param pipe [String] the transport pipe name.
264
+ # @param message [String] the message to be published to the transport.
265
+ # @yield [info] passes publish info to an optional callback/block.
266
+ # @yieldparam info [Hash] contains publish information.
267
+ # @yieldparam queued [String] current list size.
268
+ def list_publish(pipe, message, &callback)
269
+ list = redis_key("list", pipe)
270
+ redis_connection("redis").rpush(list, message) do |queued|
271
+ info = {:queued => queued}
272
+ callback.call(info) if callback
273
+ end
274
+ end
275
+
276
+ # Shift a message off of a Redis list and schedule another shift
277
+ # on the next tick of the event loop (reactor). Redis BLPOP is a
278
+ # connection blocking Redis command, this method creates a named
279
+ # Redis connection for each list. Multiple Redis connections for
280
+ # BLPOP commands is far more efficient than timer or next tick
281
+ # polling with LPOP.
282
+ #
283
+ # @param list [String]
284
+ # @yield [info, message] passes message info and content to
285
+ # the consumer/method callback/block.
286
+ # @yieldparam info [Hash] an empty hash.
287
+ # @yieldparam message [String] message content.
288
+ def list_blpop(list, &callback)
289
+ redis_connection(list).blpop(list, 0) do |_, message|
290
+ EM::next_tick {list_blpop(list, &callback)}
291
+ callback.call({}, message)
292
+ end
293
+ end
294
+
295
+ # Subscribe to a Redis list, shifting message off as they become
296
+ # available. The `redis_key()` method is used to create a Redis
297
+ # list key, using the transport pipe name. The `list_blpop()`
298
+ # method is used to do the actual work.
299
+ #
300
+ # @param pipe [String] the transport pipe name.
301
+ # @yield [info, message] passes message info and content to
302
+ # the consumer/method callback/block.
303
+ # @yieldparam info [Hash] an empty hash.
304
+ # @yieldparam message [String] message content.
305
+ def list_subscribe(pipe, &callback)
306
+ list = redis_key("list", pipe)
307
+ list_blpop(list, &callback)
308
+ end
309
+ end
310
+ end
311
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "sensu-transport"
5
- spec.version = "2.4.0"
5
+ spec.version = "3.0.0"
6
6
  spec.authors = ["Sean Porter"]
7
7
  spec.email = ["portertech@gmail.com"]
8
8
  spec.summary = "The Sensu transport abstraction library"
@@ -17,9 +17,11 @@ Gem::Specification.new do |spec|
17
17
 
18
18
  spec.add_dependency("sensu-em")
19
19
  spec.add_dependency("amqp", "1.5.0")
20
+ spec.add_dependency("em-redis-unified", ">= 1.0.0")
20
21
 
21
22
  spec.add_development_dependency "bundler", "~> 1.6"
22
23
  spec.add_development_dependency "rake"
23
24
  spec.add_development_dependency "rspec"
24
- spec.add_development_dependency "codeclimate-test-reporter"
25
+ spec.add_development_dependency "bouncy-castle-java" if RUBY_PLATFORM =~ /java/
26
+ spec.add_development_dependency "codeclimate-test-reporter" unless RUBY_VERSION < "1.9"
25
27
  end
@@ -0,0 +1,159 @@
1
+ require File.join(File.dirname(__FILE__), "helpers")
2
+ require "sensu/transport/redis"
3
+ require "logger"
4
+
5
+ describe "Sensu::Transport::Redis" do
6
+ include Helpers
7
+
8
+ before do
9
+ @transport = Sensu::Transport::Redis.new
10
+ @transport.logger = Logger.new(STDOUT)
11
+ @transport.logger.level = Logger::FATAL
12
+ end
13
+
14
+ it "provides a transport API" do
15
+ expect(@transport).to respond_to(:on_error, :before_reconnect, :after_reconnect,
16
+ :connect, :reconnect, :connected?, :close,
17
+ :publish, :subscribe, :unsubscribe,
18
+ :acknowledge, :ack, :stats)
19
+ end
20
+
21
+ it "can publish and subscribe to direct pipes" do
22
+ async_wrapper do
23
+ @transport.connect
24
+ callback = Proc.new do |info, message|
25
+ expect(info).to be_kind_of(Hash)
26
+ expect(message).to eq("msg")
27
+ timer(0.5) do
28
+ async_done
29
+ end
30
+ end
31
+ @transport.subscribe("direct", "foo", "baz", {}, &callback)
32
+ @transport.subscribe("direct", "bar", "baz", {}, &callback)
33
+ timer(1) do
34
+ @transport.publish("direct", "foo", "msg") do |info|
35
+ expect(info).to be_kind_of(Hash)
36
+ expect(info[:queued]).to eq(1)
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ it "can publish and subscribe to fanout pipes" do
43
+ async_wrapper do
44
+ @transport.connect
45
+ callback = Proc.new do |info, message|
46
+ expect(info).to be_kind_of(Hash)
47
+ expect(info[:channel]).to eq("transport:0:channel:foo")
48
+ expect(message).to eq("msg")
49
+ timer(0.5) do
50
+ async_done
51
+ end
52
+ end
53
+ @transport.subscribe("fanout", "foo", "baz", {}, &callback)
54
+ @transport.subscribe("fanout", "bar", "baz", {}, &callback)
55
+ timer(1) do
56
+ @transport.publish("fanout", "foo", "msg") do |info|
57
+ expect(info).to be_kind_of(Hash)
58
+ expect(info[:subscribers]).to eq(1)
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ it "can scope redis pubsub to the selected database" do
65
+ async_wrapper do
66
+ @transport.connect(:db => 1)
67
+ callback = Proc.new do |info, message|
68
+ expect(info).to be_kind_of(Hash)
69
+ expect(info[:channel]).to eq("transport:1:channel:foo")
70
+ async_done
71
+ end
72
+ @transport.subscribe("fanout", "foo", "baz", {}, &callback)
73
+ timer(1) do
74
+ @transport.publish("fanout", "foo", "msg")
75
+ end
76
+ end
77
+ end
78
+
79
+ it "can unsubscribe and close the connection" do
80
+ async_wrapper do
81
+ @transport.connect
82
+ @transport.subscribe("direct", "bar") do |info, message|
83
+ true
84
+ end
85
+ timer(1) do
86
+ @transport.unsubscribe do
87
+ @transport.close
88
+ timer(1) do
89
+ expect(@transport.connected?).to be(false)
90
+ async_done
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ it "can open and close the connection immediately" do
98
+ async_wrapper do
99
+ @transport.connect
100
+ @transport.close
101
+ timer(1) do
102
+ expect(@transport.connected?).to be(false)
103
+ async_done
104
+ end
105
+ end
106
+ end
107
+
108
+ it "can subscribe to a fanout pipe, reconnect, and subscribe to the same pipe again" do
109
+ async_wrapper do
110
+ @transport.connect
111
+ callback = Proc.new do |info, message|
112
+ expect(info).to be_kind_of(Hash)
113
+ expect(info[:channel]).to eq("transport:0:channel:foo")
114
+ expect(message).to eq("msg")
115
+ timer(0.5) do
116
+ async_done
117
+ end
118
+ end
119
+ @transport.subscribe("fanout", "foo", "baz", {}, &callback)
120
+ @transport.reconnect
121
+ @transport.subscribe("fanout", "foo", "baz", {}, &callback)
122
+ timer(1) do
123
+ @transport.publish("fanout", "foo", "msg") do |info|
124
+ expect(info).to be_kind_of(Hash)
125
+ expect(info[:subscribers]).to eq(1)
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ it "can get queue stats, message and consumer counts" do
132
+ async_wrapper do
133
+ @transport.connect
134
+ @transport.stats("bar") do |info|
135
+ expect(info).to be_kind_of(Hash)
136
+ expect(info[:messages]).to eq(0)
137
+ expect(info[:consumers]).to eq(0)
138
+ async_done
139
+ end
140
+ end
141
+ end
142
+
143
+ it "can fail to connect" do
144
+ async_wrapper do
145
+ @transport.connect(:port => 5555)
146
+ expect(@transport.connected?).to be(false)
147
+ async_done
148
+ end
149
+ end
150
+
151
+ it "will throw an error if it cannot resolve a hostname" do
152
+ async_wrapper do
153
+ expect {
154
+ @transport.connect(:host => "2def33c3-cfbb-4993-b5ee-08d47f6d8793")
155
+ }.to raise_error
156
+ async_done
157
+ end
158
+ end
159
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sensu-transport
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.0
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sean Porter
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-10-31 00:00:00.000000000 Z
11
+ date: 2015-05-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sensu-em
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - '='
39
39
  - !ruby/object:Gem::Version
40
40
  version: 1.5.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: em-redis-unified
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 1.0.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 1.0.0
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: bundler
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -110,6 +124,7 @@ files:
110
124
  - lib/sensu/transport.rb
111
125
  - lib/sensu/transport/base.rb
112
126
  - lib/sensu/transport/rabbitmq.rb
127
+ - lib/sensu/transport/redis.rb
113
128
  - sensu-transport.gemspec
114
129
  - spec/assets/rabbitmq.config
115
130
  - spec/assets/ssl/ca/cacert.pem
@@ -120,6 +135,7 @@ files:
120
135
  - spec/base_spec.rb
121
136
  - spec/helpers.rb
122
137
  - spec/rabbitmq_spec.rb
138
+ - spec/redis_spec.rb
123
139
  - spec/transport_spec.rb
124
140
  homepage: https://github.com/sensu/sensu-transport
125
141
  licenses:
@@ -155,4 +171,5 @@ test_files:
155
171
  - spec/base_spec.rb
156
172
  - spec/helpers.rb
157
173
  - spec/rabbitmq_spec.rb
174
+ - spec/redis_spec.rb
158
175
  - spec/transport_spec.rb