stomper 0.4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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