stomper 0.4 → 1.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.
Files changed (53) hide show
  1. data/CHANGELOG +7 -0
  2. data/README.rdoc +63 -11
  3. data/lib/stomper.rb +13 -1
  4. data/lib/stomper/client.rb +3 -284
  5. data/lib/stomper/connection.rb +67 -114
  6. data/lib/stomper/frame_reader.rb +73 -0
  7. data/lib/stomper/frame_writer.rb +21 -0
  8. data/lib/stomper/frames.rb +24 -9
  9. data/lib/stomper/frames/abort.rb +2 -6
  10. data/lib/stomper/frames/ack.rb +2 -6
  11. data/lib/stomper/frames/begin.rb +2 -5
  12. data/lib/stomper/frames/client_frame.rb +30 -27
  13. data/lib/stomper/frames/commit.rb +1 -5
  14. data/lib/stomper/frames/connect.rb +2 -7
  15. data/lib/stomper/frames/connected.rb +11 -8
  16. data/lib/stomper/frames/disconnect.rb +1 -4
  17. data/lib/stomper/frames/error.rb +3 -8
  18. data/lib/stomper/frames/message.rb +15 -11
  19. data/lib/stomper/frames/receipt.rb +1 -6
  20. data/lib/stomper/frames/send.rb +1 -5
  21. data/lib/stomper/frames/server_frame.rb +13 -23
  22. data/lib/stomper/frames/subscribe.rb +9 -14
  23. data/lib/stomper/frames/unsubscribe.rb +3 -7
  24. data/lib/stomper/open_uri_interface.rb +41 -0
  25. data/lib/stomper/receipt_handlers.rb +23 -0
  26. data/lib/stomper/receiptor.rb +38 -0
  27. data/lib/stomper/sockets.rb +37 -0
  28. data/lib/stomper/subscriber.rb +76 -0
  29. data/lib/stomper/subscription.rb +14 -14
  30. data/lib/stomper/threaded_receiver.rb +59 -0
  31. data/lib/stomper/transaction.rb +13 -8
  32. data/lib/stomper/transactor.rb +50 -0
  33. data/lib/stomper/uri.rb +55 -0
  34. data/spec/client_spec.rb +7 -158
  35. data/spec/connection_spec.rb +13 -3
  36. data/spec/frame_reader_spec.rb +37 -0
  37. data/spec/frame_writer_spec.rb +27 -0
  38. data/spec/frames/client_frame_spec.rb +22 -98
  39. data/spec/frames/indirect_frame_spec.rb +45 -0
  40. data/spec/frames/server_frame_spec.rb +15 -16
  41. data/spec/open_uri_interface_spec.rb +132 -0
  42. data/spec/receiptor_spec.rb +35 -0
  43. data/spec/shared_connection_examples.rb +12 -17
  44. data/spec/spec_helper.rb +6 -0
  45. data/spec/subscriber_spec.rb +77 -0
  46. data/spec/subscription_spec.rb +11 -11
  47. data/spec/subscriptions_spec.rb +3 -6
  48. data/spec/threaded_receiver_spec.rb +33 -0
  49. data/spec/transaction_spec.rb +5 -5
  50. data/spec/transactor_spec.rb +46 -0
  51. metadata +30 -6
  52. data/lib/stomper/frames/headers.rb +0 -68
  53. data/spec/frames/headers_spec.rb +0 -54
@@ -0,0 +1,59 @@
1
+ module Stomper
2
+ module ThreadedReceiver
3
+ def self.extended(base)
4
+ base.instance_eval do
5
+ @receiver_mutex = Mutex.new
6
+ end
7
+ end
8
+
9
+ # Starts the threaded receiver on a connection, calling receive
10
+ # on the connection repeatedly in a separate thread until the receiver
11
+ # is stopped or the connection is closed.
12
+ #
13
+ # @return self
14
+ # @see ThreadedReceiver#stop
15
+ # @see Connection#receive
16
+ # @see Connection#connected?
17
+ def start(opts={})
18
+ connect unless connected?
19
+ do_start = false
20
+ @receiver_mutex.synchronize do
21
+ do_start = !started?
22
+ end
23
+ if do_start
24
+ @started = true
25
+ @run_thread = Thread.new() do
26
+ while started? && connected?
27
+ receive
28
+ end
29
+ end
30
+ end
31
+ self
32
+ end
33
+
34
+ # Stops the threaded receiver on a connection thereby stopping further
35
+ # calls to receive.
36
+ #
37
+ # @return self
38
+ # @see ThreadedReceiver#start
39
+ # @see Connection#receive
40
+ # @see Connection#connected?
41
+ def stop
42
+ do_stop = false
43
+ @receiver_mutex.synchronize do
44
+ do_stop = started?
45
+ end
46
+ if do_stop
47
+ @started = false
48
+ @run_thread.join
49
+ @run_thread = nil
50
+ end
51
+ self
52
+ end
53
+
54
+ private
55
+ def started?
56
+ @started
57
+ end
58
+ end
59
+ end
@@ -61,7 +61,7 @@ module Stomper
61
61
  # is an instance of Stomper::Client and is required so that the Transaction
62
62
  # instance has somewhere to forward +begin+, +ack+ and +abort+ methods
63
63
  # to. If the +trans_id+ parameter is not specified, an id is automatically
64
- # generated of the form "tx-{Time.now.to_f}". This name can be accessed
64
+ # generated of the form +tx-<Time.now.to_f>+. This name can be accessed
65
65
  # through the +id+ attribute and is used in naming the transaction to
66
66
  # the stomp broker. If +block+ is given, the Transaction instance immediately
67
67
  # calls its perform method with the supplied +block+.
@@ -94,6 +94,9 @@ module Stomper
94
94
  # will raise a TransactionAborted exception if the +block+ evaluation fails.
95
95
  # This behavior allows for nesting transactions and ensuring that if a nested
96
96
  # transaction fails, so do all of its ancestors.
97
+ #
98
+ # @param [Proc] block A block of code that is evaluated as part of the transaction.
99
+ # @raise [TransactionAborted] raises an exception if the given block raises an exception
97
100
  def perform(&block) #:yields: transaction
98
101
  begin
99
102
  @client.begin(@id)
@@ -124,7 +127,7 @@ module Stomper
124
127
  # Similar to Stomper::Client#transaction, this method creates a new
125
128
  # Transaction object, nested inside of this one. To prevent name
126
129
  # collisions, this method automatically generates a transaction id,
127
- # if one is not specified, of the form "#{parent_transaction_id}-#{Time.now.to_f}.
130
+ # if one is not specified, of the form +<parent_transaction_id>-<Time.now.to_f>+.
128
131
  def transaction(transaction_id=nil,&block)
129
132
  # To get a transaction name guaranteed to not collide with this one
130
133
  # we will supply an explicit id to the constructor unless an id was
@@ -137,30 +140,32 @@ module Stomper
137
140
  # into the +headers+ hash, thus informing the stomp broker that the message
138
141
  # generated here is part of this transaction.
139
142
  def send(destination, body, headers={})
140
- headers['transaction'] = @id
141
- @client.send(destination, body, headers)
143
+ @client.send(destination, body, headers.merge({:transaction => @id }))
142
144
  end
143
145
 
144
146
  # Wraps the Stomper::Client#ack method, injecting a "transaction" header
145
147
  # into the +headers+ hash, thus informing the stomp broker that the message
146
148
  # acknowledgement is part of this transaction.
147
149
  def ack(message_or_id, headers={})
148
- headers['transaction'] = @id
149
- @client.ack(message_or_id, headers)
150
+ @client.ack(message_or_id, headers.merge({ :transaction => @id }))
150
151
  end
151
152
 
152
153
  # Aborts this transaction if it has not already been committed or aborted.
153
154
  # Note that it does so by raising a TransactionAborted exception, allowing
154
155
  # the +abort+ call to force any ancestral transactions to also fail.
155
156
  #
156
- # See also: commit, committed?, aborted?
157
+ # @see Transaction#commit
158
+ # @see Transaction#committed?
159
+ # @see Transaction#aborted?
157
160
  def abort
158
161
  raise TransactionAborted, "transaction '#{@id}' aborted explicitly" if _abort
159
162
  end
160
163
 
161
164
  # Commits this transaction unless it has already been committed or aborted.
162
165
  #
163
- # See also: abort, committed?, aborted?
166
+ # @see Transaction#abort
167
+ # @see Transaction#committed?
168
+ # @see Transaction#aborted?
164
169
  def commit
165
170
  # Guard against sending multiple commit messages to the server for a
166
171
  # single transaction.
@@ -0,0 +1,50 @@
1
+ module Stomper
2
+ module Transactor
3
+ # Creates a new Stomper::Transaction object and evaluates
4
+ # the supplied +block+ within a transactional context. If
5
+ # the block executes successfully, the transaction is committed,
6
+ # otherwise it is aborted. This method is meant to provide a less
7
+ # tedious approach to transactional messaging than the +begin+,
8
+ # +abort+ and +commit+ methods.
9
+ #
10
+ # See also: Stomper::ClientInterface::begin, Stomper::ClientInterface::commit,
11
+ # Stomper::ClientInterface::abort, Stomper::Transaction
12
+ def transaction(transaction_id=nil, &block)
13
+ begin
14
+ Stomper::Transaction.new(self, transaction_id, &block)
15
+ rescue Stomper::TransactionAborted
16
+ nil
17
+ end
18
+ end
19
+
20
+ # Tells the stomp broker to commit a transaction named by the
21
+ # supplied +transaction_id+ parameter. When used in conjunction with
22
+ # +begin+, and +abort+, a means for manually handling transactional
23
+ # message passing is provided.
24
+ #
25
+ # See Also: transaction
26
+ def commit(transaction_id)
27
+ transmit(Stomper::Frames::Commit.new(transaction_id))
28
+ end
29
+
30
+ # Tells the stomp broker to abort a transaction named by the
31
+ # supplied +transaction_id+ parameter. When used in conjunction with
32
+ # +begin+, and +commit+, a means for manually handling transactional
33
+ # message passing is provided.
34
+ #
35
+ # See Also: transaction
36
+ def abort(transaction_id)
37
+ transmit(Stomper::Frames::Abort.new(transaction_id))
38
+ end
39
+
40
+ # Tells the stomp broker to begin a transaction named by the
41
+ # supplied +transaction_id+ parameter. When used in conjunction with
42
+ # +commit+, and +abort+, a means for manually handling transactional
43
+ # message passing is provided.
44
+ #
45
+ # See also: transaction
46
+ def begin(transaction_id)
47
+ transmit(Stomper::Frames::Begin.new(transaction_id))
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,55 @@
1
+ module URI
2
+ class STOMP < ::URI::Generic
3
+ # Got to love the magic of URI::Generic.
4
+ # By setting this constant, you ensure that all
5
+ # Stomp URI's have this port if one isn't specified.
6
+ DEFAULT_PORT = 61613
7
+
8
+ def initialize(*args)
9
+ super
10
+ end
11
+
12
+ def create_socket
13
+ ::Stomper::Sockets::TCP.new(self.host||'localhost', self.port)
14
+ end
15
+
16
+ def open(*args)
17
+ conx = Stomper::Connection.open(self, :threaded_receiver => false)
18
+ conx.extend Stomper::OpenUriInterface
19
+ if block_given?
20
+ begin
21
+ yield conx
22
+ ensure
23
+ conx.disconnect
24
+ end
25
+ end
26
+ conx
27
+ end
28
+ end
29
+
30
+ class STOMP_SSL < STOMP
31
+ DEFAULT_PORT = 61612
32
+
33
+ def initialize(*args)
34
+ super
35
+ end
36
+
37
+ # Creates a socket from the URI
38
+ def create_socket
39
+ ::Stomper::Sockets::SSL.new(self.host||'localhost', self.port)
40
+ end
41
+
42
+ # The +uri+ standard library resolves string URI's to concrete classes
43
+ # by matching the string's schema to the name of a subclass of URI::Generic.
44
+ # Ruby doesn't support '+' symbols in a class name, so the only way to handle
45
+ # schemas with odd characters is to override the "to_s" function of the class.
46
+ #
47
+ # Why do I get the feeling this might be a bad idea?
48
+ def self.to_s
49
+ "URI::STOMP+SSL"
50
+ end
51
+ end
52
+
53
+ @@schemes['STOMP'] = STOMP
54
+ @@schemes['STOMP+SSL'] = STOMP_SSL
55
+ end
data/spec/client_spec.rb CHANGED
@@ -2,179 +2,28 @@ require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
2
2
 
3
3
  module Stomper
4
4
  describe Client do
5
+ class MockConcreteClient
6
+ include Stomper::Client
7
+ end
8
+
5
9
  before(:each) do
6
- # For the client, we want to mock the underlying connection
7
- @mock_connection = mock("connection")
8
- @mock_connection.should_receive(:disconnect).with(no_args()).at_most(:once).and_return(nil)
9
- Stomper::Connection.stub!(:new).and_return(@mock_connection)
10
- @client = Client.new("stomp:///")
10
+ @client = MockConcreteClient.new
11
11
  end
12
12
 
13
13
  describe "expected interface" do
14
14
  it "should provide a send method" do
15
15
  @client.should respond_to(:send)
16
- @mock_connection.should_receive(:transmit).with(an_instance_of(Stomper::Frames::Send)).twice.and_return(nil)
16
+ @client.should_receive(:transmit).with(an_instance_of(Stomper::Frames::Send)).twice.and_return(nil)
17
17
  @client.send("/queue/to", "message body", {:additional => 'header'})
18
18
  @client.send("/queue/to", "message body")
19
19
  end
20
- it "should provide a subscribe method" do
21
- @client.should respond_to(:subscribe)
22
- @mock_connection.should_receive(:transmit).with(an_instance_of(Stomper::Frames::Subscribe)).twice.and_return(nil)
23
- @client.subscribe("/queue/to", {:additional => 'header'})
24
- @client.subscribe("/queue/to")
25
- end
26
- it "should provide an unsubscribe method" do
27
- @client.should respond_to(:unsubscribe)
28
- @mock_connection.should_receive(:transmit).with(an_instance_of(Stomper::Frames::Subscribe)).twice.and_return(nil)
29
- @client.subscribe("/queue/to", {:id => 'subscription-id'})
30
- @client.subscribe("/queue/to")
31
- @mock_connection.should_receive(:transmit).with(an_instance_of(Stomper::Frames::Unsubscribe)).twice.and_return(nil)
32
- @client.unsubscribe("/queue/to", 'subscription-id')
33
- @client.unsubscribe("/queue/to")
34
- end
35
20
  it "should provide an ack method" do
36
21
  @client.should respond_to(:ack)
37
- @mock_connection.should_receive(:transmit).with(an_instance_of(Stomper::Frames::Ack)).exactly(3).times.and_return(nil)
22
+ @client.should_receive(:transmit).with(an_instance_of(Stomper::Frames::Ack)).exactly(3).times.and_return(nil)
38
23
  @client.ack("message-id", {:additional => "header"})
39
24
  @client.ack("message-id")
40
25
  @client.ack(Stomper::Frames::Message.new({:'message-id' => 'msg-001'}, "body"))
41
26
  end
42
- it "should provide a begin method" do
43
- @client.should respond_to(:begin)
44
- @mock_connection.should_receive(:transmit).with(an_instance_of(Stomper::Frames::Begin)).once.and_return(nil)
45
- @client.begin("tx-001")
46
- end
47
- it "should proivde an abort method" do
48
- @client.should respond_to(:abort)
49
- @mock_connection.should_receive(:transmit).with(an_instance_of(Stomper::Frames::Abort)).once.and_return(nil)
50
- @client.abort("tx-001")
51
- end
52
- it "should provide a commit method" do
53
- @client.should respond_to(:commit)
54
- @mock_connection.should_receive(:transmit).with(an_instance_of(Stomper::Frames::Commit)).once.and_return(nil)
55
- @client.commit("tx-001")
56
- end
57
- it "should provide a recieve method" do
58
- @client.should respond_to(:receive)
59
- end
60
- it "should provide a disconnect method" do
61
- @client.should respond_to(:disconnect)
62
- end
63
- it "should provide a close method" do
64
- @client.should respond_to(:close)
65
- end
66
- it "should provide a connectivity test" do
67
- @client.should respond_to(:connected?)
68
- end
69
- it "should provide a connect method" do
70
- @client.should respond_to(:connect)
71
- end
72
- end
73
-
74
- describe "threaded receiver" do
75
- it "should respond to start and stop" do
76
- @client.should respond_to(:start)
77
- @client.should respond_to(:stop)
78
- @client.should respond_to(:receiving?)
79
- end
80
- it "should only be receiving when it is started" do
81
- @mock_connection.stub!(:receive).and_return(nil)
82
- @mock_connection.should_receive(:connected?).any_number_of_times.and_return(true)
83
- @client.receiving?.should be_false
84
- @client.start
85
- @client.receiving?.should be_true
86
- @client.stop
87
- @client.receiving?.should be_false
88
- end
89
- it "should allow for a blocking threaded receiver" do
90
- @mock_connection.should_receive(:receive).with(true).at_least(:once).and_return(nil)
91
- @mock_connection.should_receive(:connected?).any_number_of_times.and_return(true)
92
- @client.receiving?.should be_false
93
- @client.start(:block => true)
94
- @client.receiving?.should be_true
95
- @client.stop
96
- @client.receiving?.should be_false
97
- end
98
-
99
- end
100
-
101
- describe "subscribing to queue" do
102
- before(:each) do
103
- @message_sent = Stomper::Frames::Message.new({'destination' => "/queue/test"}, "test message")
104
- @mock_connection.should_receive(:connected?).any_number_of_times.and_return(true)
105
- @mock_connection.should_receive(:transmit).with(duck_type(:to_stomp)).at_least(:once).and_return(nil)
106
- @mock_connection.should_receive(:receive).any_number_of_times.and_return(@message_sent)
107
- end
108
-
109
- it "should subscribe to a destination with a block" do
110
- wait_for_message = true
111
- @message_received = nil
112
- @client.start
113
- @client.subscribe("/queue/test") do |msg|
114
- @message_received = msg
115
- wait_for_message = false
116
- end
117
- true while wait_for_message
118
- @client.stop
119
- @message_received.should == @message_sent
120
- end
121
-
122
- it "should not unsubscribe from all destinations when a subscription id is provided" do
123
- @client.subscribe("/queue/test", { 'id' => 'subscription-1' }) do |msg|
124
- @message_received = msg
125
- end
126
- @client.subscribe("/queue/test") do |msg|
127
- @message_received = msg
128
- end
129
- @client.subscribe("/queue/test", :id => 'subscription-2') do |msg|
130
- @message_received = msg
131
- end
132
- @client.unsubscribe("/queue/test", 'subscription-1')
133
- @client.subscriptions.size.should == 2
134
- end
135
-
136
- it "should not unsubscribe from non-naive subscriptions when only a destination is supplied" do
137
- @client.subscribe("/queue/test", { 'id' => 'subscription-1' }) do |msg|
138
- @message_received = msg
139
- end
140
- @client.subscribe("/queue/test") do |msg|
141
- @message_received = msg
142
- end
143
- @client.subscribe("/queue/test") do |msg|
144
- @message_received = msg
145
- end
146
- @client.unsubscribe("/queue/test")
147
- @client.subscriptions.size.should == 1
148
- end
149
-
150
- # Due to the receiver running in a separate thread, this may not be correct?
151
- it "should unsubscribe from a destination and receive no more messages" do
152
- @mutex = Mutex.new
153
- @last_message_received = nil
154
- @client.start
155
- @client.subscribe("/queue/test") do |msg|
156
- @last_message_received = Time.now
157
- end
158
- true until @last_message_received
159
- @client.unsubscribe("/queue/test")
160
- @unsubscribed_at = Time.now
161
- @client.stop
162
- (@last_message_received < @unsubscribed_at).should be_true
163
- end
164
- end
165
-
166
- describe "transactions" do
167
- before(:each) do
168
- @mock_connection.should_receive(:transmit).with(duck_type(:to_stomp)).at_least(:once).and_return(nil)
169
- end
170
-
171
- it "should provide a transaction method that generates a new Transaction" do
172
- @evaluated = false
173
- @client.transaction do |t|
174
- @evaluated = true
175
- end
176
- @evaluated.should be_true
177
- end
178
27
  end
179
28
  end
180
29
  end
@@ -3,10 +3,20 @@ require File.expand_path(File.join(File.dirname(__FILE__), 'shared_connection_ex
3
3
 
4
4
  module Stomper
5
5
  describe Connection do
6
- before(:each) do
7
- @connection = Connection.new("stomp:///", :connect_now => false)
6
+ describe "standard connection" do
7
+ before(:each) do
8
+ @connection = Connection.new("stomp:///")
9
+ end
10
+
11
+ it_should_behave_like "All Client Connections"
8
12
  end
9
13
 
10
- it_should_behave_like "All Client Connections"
14
+ describe "ssl connection" do
15
+ before(:each) do
16
+ @connection = Connection.new("stomp+ssl:///")
17
+ end
18
+
19
+ it_should_behave_like "All Client Connections"
20
+ end
11
21
  end
12
22
  end
@@ -0,0 +1,37 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ module Stomper
4
+ describe FrameReader do
5
+ before(:each) do
6
+ @input_stream = StringIO.new("", "w+")
7
+ @input_stream.send(:extend, Stomper::FrameReader)
8
+ end
9
+
10
+ it "should produce a stomper frame" do
11
+ @input_stream.string = "CONNECTED\n\n\0"
12
+ @input_stream.receive_frame.should be_an_instance_of(Stomper::Frames::Connected)
13
+ end
14
+
15
+ it "should read headers appropriately" do
16
+ @input_stream.string = "CONNECTED\nheader_1:a test value\nheader_2:another test value\nblather:47\n\nthe frame body\0"
17
+ @frame = @input_stream.receive_frame
18
+ @frame.headers.map { |(k,v)|
19
+ [k,v]
20
+ }.sort { |a, b| a.first.to_s <=> b.first.to_s }.should == [ [:blather, '47'], [:header_1, 'a test value'], [:header_2, 'another test value'] ]
21
+ end
22
+
23
+ it "should raise an exception when an invalid content-length is specified" do
24
+ @input_stream.string = "CONNECTED\ncontent-length:3\n\nsomething more than 3 bytes long\0"
25
+ lambda { @input_stream.receive_frame }.should raise_error(Stomper::MalformedFrameError)
26
+ end
27
+
28
+ it "should read the body of a message when a content length is specified" do
29
+ @input_stream.string = "CONNECTED\ncontent-length:6\n\na test\0followed by trailing nonsense"
30
+ @input_stream.receive_frame.body.should == "a test"
31
+ end
32
+ it "should read the body of a message when no content length is specified" do
33
+ @input_stream.string = "CONNECTED\n\na bit more text and no direction\0followed by trailing nonsense"
34
+ @input_stream.receive_frame.body.should == "a bit more text and no direction"
35
+ end
36
+ end
37
+ end