sensu-transport 2.4.0 → 3.0.0

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: 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