stomper 2.0.1 → 2.0.2

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.
data/CHANGELOG.md ADDED
@@ -0,0 +1,28 @@
1
+ # Changes
2
+
3
+ ## 2.0.2 - 2011-03-01
4
+
5
+ * updated to-do's and version number in README.md
6
+
7
+ * added CHANGELOG.md
8
+
9
+ * connection's @close@ method now clears remaining subscriptions and receipt
10
+ handlers after all events are fired. should ease the work
11
+ the failover extension has to do to re-subscribe appropriately.
12
+
13
+ * corrected spec tests to handle features introduced in 2.0.1 - should not
14
+ have pushed a new version until those specs were run.
15
+
16
+ ## 2.0.1 - 2011-02-27
17
+
18
+ * connection now raises exception in threaded receiver to stop, prevents
19
+ blocking on @receive@
20
+
21
+ * connection terminated event now fires properly, fixes #1
22
+
23
+ * subscription manager clears remaining subscriptions when the connection
24
+ is closed, fixes #2
25
+
26
+ ## 2.0.0 - 2011-02-22
27
+
28
+ * Rewrite of stomper
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- #Stomper 2.0.0
1
+ #Stomper 2.0
2
2
 
3
3
  Stomper is a library for connecting to and interacting with a message broker
4
4
  service that supports the Stomp [1.0](http://stomp.github.com/stomp-specification-1-0.html)
@@ -15,22 +15,6 @@ rather unpleasant to deal with for common use cases.
15
15
  This gem is in no way related to the [Python stomper](http://code.google.com/p/stomper/)
16
16
  library.
17
17
 
18
- ##Still in the Works
19
-
20
- There are a few features that have not yet been implemented, hence why we're
21
- still on a prerelease version number. The following is a list of the major
22
- deficiencies of the gem, but this list is not necessarily exhaustive.
23
-
24
- * Write some features to test other connections:
25
- * Authenticated logins
26
- * SSL Client verification (just to ensure that SSL params are being
27
- delivered appropriately)
28
- * A bit of refactoring is in order
29
- * Refactor specs a bit, clean up separation between 1.9 and 1.8 specific tests
30
-
31
- As these issues are resolved, I'll drop them from the list (and potentially
32
- add other issues)
33
-
34
18
  ##Example Usage
35
19
 
36
20
  # Establish a connection to the broker
@@ -5,17 +5,17 @@ Feature: Protocol version negotiation
5
5
 
6
6
  Scenario: By default, allow 1.1 from broker
7
7
  Given a Stomp 1.1 broker
8
- When a connection is established
8
+ When the connection is told to connect
9
9
  Then the connection should be using the 1.1 protocol
10
10
 
11
11
  Scenario: By default, allow 1.0 from broker
12
12
  Given a Stomp 1.0 broker
13
- When a connection is established
13
+ When the connection is told to connect
14
14
  Then the connection should be using the 1.0 protocol
15
15
 
16
16
  Scenario: By default, assume 1.0 from version-less broker
17
17
  Given an unversioned Stomp broker
18
- When a connection is established
18
+ When the connection is told to connect
19
19
  Then the connection should be using the 1.0 protocol
20
20
 
21
21
  Scenario: By default, raise error if the broker's version isn't supported
@@ -26,13 +26,13 @@ Feature: Protocol version negotiation
26
26
  Scenario: A 1.0 client should accept a 1.0 broker
27
27
  Given a Stomp 1.0 broker
28
28
  When the client protocol version is "1.0"
29
- And a connection is established
29
+ And the connection is told to connect
30
30
  Then the connection should be using the 1.0 protocol
31
31
 
32
32
  Scenario: A 1.0 client should accept a version-less broker
33
33
  Given an unversioned Stomp broker
34
34
  When the client protocol version is "1.0"
35
- And a connection is established
35
+ And the connection is told to connect
36
36
  Then the connection should be using the 1.0 protocol
37
37
 
38
38
  Scenario: A 1.0 client should not accept a 1.1 broker
@@ -44,7 +44,7 @@ Feature: Protocol version negotiation
44
44
  Scenario: A 1.1 client should accept a 1.1 broker
45
45
  Given a Stomp 1.1 broker
46
46
  When the client protocol version is "1.1"
47
- And a connection is established
47
+ And the connection is told to connect
48
48
  Then the connection should be using the 1.1 protocol
49
49
 
50
50
  Scenario: A 1.1 client should not accept a 1.0 broker
@@ -38,4 +38,12 @@ end
38
38
 
39
39
  Then /^the client nacking the last MESSAGE should raise an argument error$/ do
40
40
  lambda { @connection.nack @received_frames.select { |f| f.command == "MESSAGE" }.last }.should raise_error(ArgumentError)
41
- end
41
+ end
42
+
43
+ When /^the client acks a message by ID "([^"]*)" and subscription "([^"]*)" within the scope$/ do |message_id, subscription|
44
+ @scope.ack message_id, subscription
45
+ end
46
+
47
+ When /^the client nacks a message by ID "([^"]*)" and subscription "([^"]*)" within the scope$/ do |message_id, subscription|
48
+ @scope.nack message_id, subscription
49
+ end
@@ -0,0 +1,52 @@
1
+ Given /^a Stomp (\d+\.\d+)?\s*broker$/ do |version|
2
+ version ||= '1.0'
3
+ @broker_uri_string = "stomp:///"
4
+ @broker_uri = URI.parse(@broker_uri_string)
5
+ @broker = TestStompServer.new(version)
6
+ @broker.start
7
+ @connection = Stomper::Connection.new(@broker_uri)
8
+ end
9
+
10
+ Given /^an erroring Stomp broker$/ do
11
+ @broker_uri_string = "stomp:///"
12
+ @broker_uri = URI.parse(@broker_uri_string)
13
+ @broker = TestStompServer.new('1.0')
14
+ @broker.session_class = TestStompServer::StompErrorOnConnectSession
15
+ @broker.start
16
+ @connection = Stomper::Connection.new(@broker_uri)
17
+ end
18
+
19
+ Then /^the broker should have received an? "([^"]*)" frame$/ do |command|
20
+ Then "the broker should have received a \"#{command}\" frame with headers", table(%{
21
+ | header-name | header-value |
22
+ })
23
+ end
24
+
25
+ Then /^the broker should have received an? "([^"]*)" frame with headers$/ do |command, table|
26
+ headers = table_to_headers table
27
+ @broker.session.received_frames.any? do |f|
28
+ f.command == command && headers.all? { |(k,v)| headers[k] == f[k] }
29
+ end.should be_true
30
+ end
31
+
32
+ When /^the broker sends a "([^"]*)" frame with headers$/ do |command, table|
33
+ headers = table_to_headers table
34
+ @broker.session.send_frame command, headers
35
+ end
36
+
37
+ When /^the broker closes the connection unexpectedly$/ do
38
+ @broker.force_stop
39
+ end
40
+
41
+ Given /^a Stomp (\d+\.\d+)?\s*SSL broker$/ do |version|
42
+ @broker = TestSSLStompServer.new(version)
43
+ @broker.start
44
+ end
45
+
46
+ Given /^an unversioned Stomp broker$/ do
47
+ @broker_uri_string = "stomp:///"
48
+ @broker_uri = URI.parse(@broker_uri_string)
49
+ @broker = TestStompServer.new(nil)
50
+ @broker.start
51
+ @connection = Stomper::Connection.new(@broker_uri)
52
+ end
@@ -0,0 +1,137 @@
1
+ After do |s|
2
+ begin
3
+ @connection && @connection.stop
4
+ @broker && @broker.force_stop
5
+ rescue Exception => ex
6
+ end
7
+ end
8
+
9
+ Given /^a (\d+\.\d+)?\s*connection between client and broker$/ do |version|
10
+ version ||= '1.0'
11
+ @broker_uri_string = "stomp:///"
12
+ @broker_uri = URI.parse(@broker_uri_string)
13
+ @broker = TestStompServer.new(version)
14
+ @broker.start
15
+ @connection = Stomper::Connection.new(@broker_uri)
16
+ @received_frames = []
17
+ @sent_frames = []
18
+ @connection.before_transmitting do |f, c|
19
+ @sent_frames << f
20
+ end
21
+ @connection.after_receiving do |f, c|
22
+ @received_frames << f
23
+ end
24
+ @connection.start
25
+ end
26
+
27
+ When /^the frame exchange is completed$/ do
28
+ @connection.disconnect(:receipt => 'TERMINATE_POLITELY_12345')
29
+ @connection.stop
30
+ @broker.stop
31
+ end
32
+
33
+ When /^the frame exchange is completed without client disconnect$/ do
34
+ @connection.stop
35
+ @broker.stop
36
+ end
37
+
38
+ When /^a connection is created from the broker's URI$/ do
39
+ #@connection = Stomper::Connection.new(@broker_uri)
40
+ end
41
+
42
+ When /^a connection is created from the broker's URI string$/ do
43
+ @connection = Stomper::Connection.new(@broker_uri_string)
44
+ end
45
+
46
+ When /^the connection is told to connect$/ do
47
+ @connection.connect
48
+ end
49
+
50
+ When /^the client protocol version is "([^"]*)"$/ do |arg1|
51
+ @connection.versions = arg1.split(",")
52
+ end
53
+
54
+ Then /^the connection should be connected$/ do
55
+ @connection.connected?.should be_true
56
+ end
57
+
58
+ Then /^the connection should be using the (\d+\.\d+) protocol$/ do |version|
59
+ @connection.version.should == version
60
+ end
61
+
62
+ Then /^connecting should raise an unsupported protocol version error$/ do
63
+ lambda { @connection.connect }.should raise_error(Stomper::Errors::UnsupportedProtocolVersionError)
64
+ end
65
+
66
+ Then /^the (connection|client) should not be connected$/ do |arbitrary_name|
67
+ @connection.connected?.should be_false
68
+ end
69
+
70
+ Then /^connecting should raise an connect failed error$/ do
71
+ lambda { @connection.connect }.should raise_error(Stomper::Errors::ConnectFailedError)
72
+ end
73
+
74
+ Then /^the client should have received an? "([^"]*)" frame with headers$/ do |command, table|
75
+ headers = table_to_headers table
76
+ @received_frames.any? do |f|
77
+ f.command == command && headers.all? { |(k,v)| headers[k] == f[k] }
78
+ end.should be_true
79
+ end
80
+
81
+ When /^the client waits for (\d+) "([^"]*)" frames?$/ do |count, command|
82
+ count = count.to_i
83
+ Thread.pass while @received_frames.select { |f| f.command == command }.size < count
84
+ end
85
+
86
+ Given /^an established connection$/ do
87
+ @connection.connect
88
+ end
89
+
90
+ When /^the client disconnects$/ do
91
+ @connection.disconnect
92
+ @broker.stop
93
+ end
94
+
95
+ Then /^after (\d+\.\d+) seconds, the receiver should no longer be running$/ do |sleep_for|
96
+ sleep sleep_for.to_f
97
+ @connection.running?.should be_false
98
+ end
99
+
100
+ When /^a connection is created for the SSL broker$/ do
101
+ @connection = Stomper::Connection.new("stomp+ssl:///")
102
+ end
103
+
104
+ When /^the broker's host is "([^"]*)"$/ do |hostname|
105
+ @connection.host = hostname
106
+ end
107
+
108
+ When /^no SSL verification is performed$/ do
109
+ @connection.ssl[:verify_mode] = OpenSSL::SSL::VERIFY_NONE
110
+ end
111
+
112
+ When /^SSL verification is performed$/ do
113
+ @connection.ssl[:verify_mode] = ::OpenSSL::SSL::VERIFY_PEER | ::OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
114
+ end
115
+
116
+ When /^an SSL post connection check is performed on "([^"]*)"$/ do |host|
117
+ @connection.ssl[:post_connection_check] = host
118
+ end
119
+
120
+ Then /^connecting should raise an openssl error$/ do
121
+ lambda { @connection.connect }.should raise_error(OpenSSL::SSL::SSLError)
122
+ # It is problematic that this is needed...
123
+ @broker.stop
124
+ end
125
+
126
+ When /^an SSL post connection check is not performed$/ do
127
+ @connection.ssl[:post_connection_check] = false
128
+ end
129
+
130
+ When /^the broker's certificate is verified by CA$/ do
131
+ @connection.ssl[:ca_file] = File.expand_path('../../support/ssl/demoCA/cacert.pem', __FILE__)
132
+ end
133
+
134
+ When /^the client's certificate and key are specified$/ do
135
+ @connection.ssl[:cert] = OpenSSL::X509::Certificate.new(File.read(File.expand_path('../../support/ssl/client_cert.pem', __FILE__)))
136
+ @connection.ssl[:key] = OpenSSL::PKey::RSA.new(File.read(File.expand_path('../../support/ssl/client_key.pem', __FILE__)))
137
+ end
@@ -3,19 +3,6 @@ Given /^a header scope with headers$/ do |table|
3
3
  @scope = @connection.with_headers(headers)
4
4
  end
5
5
 
6
- When /^the client acks a message by ID "([^"]*)" and subscription "([^"]*)" within the scope$/ do |message_id, subscription|
7
- @scope.ack message_id, subscription
8
- end
9
-
10
- When /^the client subscribes to "([^"]*)" with headers within the scope$/ do |dest, table|
11
- @subscribe_frames ||= []
12
- headers = table_to_headers table
13
- @default_subscription_triggered = 0
14
- @subscribe_frames << @scope.subscribe(dest, headers) do |m|
15
- @default_subscription_triggered += 1
16
- end
17
- end
18
-
19
6
  Given /^a transaction scope named "([^"]*)"$/ do |tx|
20
7
  @scope = @connection.with_transaction(:transaction => tx)
21
8
  end
@@ -24,9 +11,6 @@ When /^the client begins the transaction scope$/ do
24
11
  @scope.begin
25
12
  end
26
13
 
27
- When /^the client nacks a message by ID "([^"]*)" and subscription "([^"]*)" within the scope$/ do |message_id, subscription|
28
- @scope.nack message_id, subscription
29
- end
30
14
 
31
15
  When /^the client aborts the transaction scope$/ do
32
16
  @scope.abort
@@ -0,0 +1,80 @@
1
+ When /^the client subscribes to "([^"]*)" with headers$/ do |dest, table|
2
+ @subscribe_frames ||= []
3
+ headers = table_to_headers table
4
+ @default_subscription_triggered = 0
5
+ @subscribe_frames << @connection.subscribe(dest, headers) do |m|
6
+ @default_subscription_triggered += 1
7
+ end
8
+ end
9
+
10
+ Then /^the default subscription callback should have been triggered( (\d+) times?)?$/ do |full, times|
11
+ if times.nil? || times.empty?
12
+ @default_subscription_triggered.should >= 1
13
+ else
14
+ @default_subscription_triggered.should == times.to_i
15
+ end
16
+ end
17
+
18
+ Then /^the default subscription callback should not have been triggered$/ do
19
+ @default_subscription_triggered.should == 0
20
+ end
21
+
22
+ When /^the client unsubscribes by ID$/ do
23
+ @connection.unsubscribe(@subscribe_frames.last[:id])
24
+ end
25
+
26
+ When /^the client unsubscribes by destination$/ do
27
+ @connection.unsubscribe(@subscribe_frames.last[:destination])
28
+ end
29
+
30
+ When /^the client unsubscribes by frame$/ do
31
+ @connection.unsubscribe(@subscribe_frames.last)
32
+ end
33
+
34
+ When /^the client unsubscribes from destination "([^"]*)"$/ do |destination|
35
+ @connection.unsubscribe(destination)
36
+ end
37
+
38
+ Given /^the client subscribes to (\/.*)$/ do |dest|
39
+ @messages_for_subscription ||= []
40
+ @connection.subscribe(dest) do |m|
41
+ sub = m[:subscription]
42
+ @messages_for_subscription << m
43
+ end
44
+ end
45
+
46
+ When /^the client sends a "([^"]*)" "([^"]*)" to (\/.*)$/ do |ct, body, dest|
47
+ @connection.send(dest, body, :'content-type' => ct)
48
+ end
49
+
50
+ Then /^the client should have received a "([^"]*)" message of "([^"]*)"$/ do |ct, body|
51
+ @messages_for_subscription.any? do |m|
52
+ m.content_type == ct && m.body == body
53
+ end.should be_true
54
+ end
55
+
56
+ When /^the client sends a "([^"]*)" encoded as "([^"]*)" to (\/.*)$/ do |body, enc, dest|
57
+ body.force_encoding(enc) if body.respond_to?(:force_encoding)
58
+ @connection.send(dest, body)
59
+ end
60
+
61
+ Then /^the client should have received a "([^"]*)" message of "([^"]*)" encoded as "([^"]*)"$/ do |ct, body, enc|
62
+ @messages_for_subscription.any? do |m|
63
+ ct_check = (m.content_type == ct || m.content_type.nil? && ct.empty?)
64
+ b_check = body == m.body
65
+ if body.respond_to?(:encoding)
66
+ ct_check && b_check && m.body.encoding.name == enc
67
+ else
68
+ ct_check && b_check
69
+ end
70
+ end.should be_true
71
+ end
72
+
73
+ When /^the client subscribes to "([^"]*)" with headers within the scope$/ do |dest, table|
74
+ @subscribe_frames ||= []
75
+ headers = table_to_headers table
76
+ @default_subscription_triggered = 0
77
+ @subscribe_frames << @scope.subscribe(dest, headers) do |m|
78
+ @default_subscription_triggered += 1
79
+ end
80
+ end
@@ -171,7 +171,7 @@ class Stomper::Connection
171
171
  @connecting = false
172
172
  @disconnecting = false
173
173
  @disconnected = false
174
- @socket_mutex = ::Mutex.new
174
+ @close_mutex = ::Mutex.new
175
175
 
176
176
  on_connected do |cf, con|
177
177
  unless connected?
@@ -272,34 +272,32 @@ class Stomper::Connection
272
272
  # connection has been established and you're ready to go, otherwise the
273
273
  # socket will be closed and an error will be raised.
274
274
  def connect(headers={})
275
- #@socket_mutex.synchronize do
276
- unless @connected
277
- @socket = @uri.create_socket(@ssl)
278
- @serializer = ::Stomper::FrameSerializer.new(@socket)
279
- m_headers = {
280
- :'accept-version' => @versions.join(','),
281
- :host => @host,
282
- :'heart-beat' => @heartbeats.join(','),
283
- :login => @login,
284
- :passcode => @passcode
285
- }
286
- @disconnecting = false
287
- @disconnected = false
288
- @connecting = true
289
- transmit create_frame('CONNECT', headers, m_headers)
290
- receive.tap do |f|
291
- if f.command == 'CONNECTED'
292
- @connected_frame = f
293
- @connected = true
294
- @connecting = false
295
- trigger_event(:on_connection_established, self)
296
- else
297
- close
298
- raise ::Stomper::Errors::ConnectFailedError, 'broker did not send CONNECTED frame'
299
- end
275
+ unless @connected
276
+ @socket = @uri.create_socket(@ssl)
277
+ @serializer = ::Stomper::FrameSerializer.new(@socket)
278
+ m_headers = {
279
+ :'accept-version' => @versions.join(','),
280
+ :host => @host,
281
+ :'heart-beat' => @heartbeats.join(','),
282
+ :login => @login,
283
+ :passcode => @passcode
284
+ }
285
+ @disconnecting = false
286
+ @disconnected = false
287
+ @connecting = true
288
+ transmit create_frame('CONNECT', headers, m_headers)
289
+ receive.tap do |f|
290
+ if f.command == 'CONNECTED'
291
+ @connected_frame = f
292
+ @connected = true
293
+ @connecting = false
294
+ trigger_event(:on_connection_established, self)
295
+ else
296
+ close
297
+ raise ::Stomper::Errors::ConnectFailedError, 'broker did not send CONNECTED frame'
300
298
  end
301
299
  end
302
- #end
300
+ end
303
301
  end
304
302
  alias :open :connect
305
303
 
@@ -372,7 +370,7 @@ class Stomper::Connection
372
370
  # @param [true,false] fire_terminated (false) If true, trigger
373
371
  # {Stomper::Extensions::Events#on_connection_terminated}
374
372
  def close
375
- #@socket_mutex.synchronize do
373
+ @close_mutex.synchronize do
376
374
  if @connected
377
375
  begin
378
376
  trigger_event(:on_connection_terminated, self) unless @disconnected
@@ -384,8 +382,10 @@ class Stomper::Connection
384
382
  @connected = false
385
383
  end
386
384
  trigger_event(:on_connection_closed, self)
385
+ subscription_manager.clear
386
+ receipt_manager.clear
387
387
  end
388
- #end
388
+ end
389
389
  end
390
390
 
391
391
  # Transmits a frame to the broker. This is a low-level method used internally
@@ -27,6 +27,11 @@ class Stomper::ReceiptManager
27
27
  self
28
28
  end
29
29
 
30
+ # Remove all receipt handlers.
31
+ def clear
32
+ @mon.synchronize { @callbacks.clear }
33
+ end
34
+
30
35
  private
31
36
  def dispatch(receipt)
32
37
  cb = @mon.synchronize { @callbacks.delete(receipt[:'receipt-id']) }
@@ -9,7 +9,6 @@ class Stomper::SubscriptionManager
9
9
  @subscriptions = {}
10
10
  connection.on_message { |m, con| dispatch(m) }
11
11
  connection.on_unsubscribe { |u, con| remove(u[:id]) }
12
- connection.on_connection_closed { |con| @subscriptions.clear }
13
12
  end
14
13
 
15
14
  # Adds a callback handler for a MESSAGE frame that is sent via the subscription
@@ -46,12 +45,18 @@ class Stomper::SubscriptionManager
46
45
  end
47
46
  end
48
47
 
49
- # Returns all current subscriptions in the form of their SUBSCRIBE frames.
50
- # @return [Array<Stomper::Frame>]
48
+ # Returns all current subscriptions.
49
+ # @return [Array<Stomper::SubscriptionManager::Subscription>]
51
50
  def subscriptions
52
51
  @mon.synchronize { @subscriptions.values }
53
52
  end
54
53
 
54
+ # Remove all subscriptions. This method does not send UNSUBSCRIBE frames
55
+ # to the broker.
56
+ def clear
57
+ @mon.synchronize { @subscriptions.clear }
58
+ end
59
+
55
60
  private
56
61
  def dispatch(message)
57
62
  s_id = message[:subscription]
@@ -3,5 +3,5 @@
3
3
  # Primary namespace of the stomper gem.
4
4
  module Stomper
5
5
  # The current version of the stomper gem.
6
- VERSION = '2.0.1'
6
+ VERSION = '2.0.2'
7
7
  end
@@ -218,6 +218,22 @@ module Stomper
218
218
  @connection.connected?.should be_false
219
219
  end
220
220
 
221
+ it "should clear subscriptions on close" do
222
+ @serializer.should_receive(:read_frame).at_least(:once).and_return(@connected_frame)
223
+ @serializer.should_receive(:write_frame).with(stomper_frame_with_headers({}, 'CONNECT')).once.and_return { |f| f }
224
+ @connection.connect
225
+ @connection.subscription_manager.should_receive(:clear)
226
+ @connection.close
227
+ end
228
+
229
+ it "should clear receipt handlers on close" do
230
+ @serializer.should_receive(:read_frame).at_least(:once).and_return(@connected_frame)
231
+ @serializer.should_receive(:write_frame).with(stomper_frame_with_headers({}, 'CONNECT')).once.and_return { |f| f }
232
+ @connection.connect
233
+ @connection.receipt_manager.should_receive(:clear)
234
+ @connection.close
235
+ end
236
+
221
237
  describe "frame reading" do
222
238
  before(:each) do
223
239
  @serializer.should_receive(:read_frame).at_least(:once).and_return(@connected_frame)
@@ -394,7 +410,6 @@ module Stomper
394
410
  lambda { @connection.receive }.should raise_error(SystemCallError)
395
411
  triggered.should be_true
396
412
  end
397
-
398
413
  end
399
414
 
400
415
  describe "frame events" do