stomper 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,180 @@
1
+ module Stomper
2
+ # An exception raised whenever a Transaction object has been aborted
3
+ # due to an unhandled exception generated by its supplied block, or when
4
+ # the block explicitly aborts the transaction.
5
+ #
6
+ # See also: Stomper::Transaction#perform
7
+ class TransactionAborted < RuntimeError; end
8
+
9
+ # An encapsulation of a stomp transaction. Manually managing transactions
10
+ # is possible through the use of Stomper::Client#begin, Stomper::Client#commit,
11
+ # and Stomper::Client#abort.
12
+ #
13
+ # === Example Usage
14
+ #
15
+ # When the transaction is passed to the block:
16
+ #
17
+ # client.transaction do |t|
18
+ # t.send("/queue/target", "doing some work")
19
+ #
20
+ # # do something that might raise an exception, indicating that any
21
+ # # messages and acknowledgements we have sent should be "undone"
22
+ #
23
+ # t.send("/queue/target", "completed work")
24
+ # end
25
+ #
26
+ # When the block is evaluated within the transaction:
27
+ #
28
+ # client.transaction do
29
+ # send("/queue/target", "doing some work")
30
+ #
31
+ # # ...
32
+ #
33
+ # send("/queue/target", "completed work")
34
+ # end
35
+ #
36
+ # Nesting transactions:
37
+ #
38
+ # client.transaction do |t|
39
+ # t.transaction do |nt|
40
+ # nt.send("/queue/target", ...)
41
+ #
42
+ # nt.transaction do |nnt|
43
+ # nnt.send("/queue/target", ...)
44
+ #
45
+ # # do something with potentially exceptional results
46
+ # end
47
+ #
48
+ # nt.send("/queue/target", ...)
49
+ # end
50
+ #
51
+ # t.send("/queue/target", ...)
52
+ # end
53
+ #
54
+ # See also: Stomper::Client#transaction
55
+ #
56
+ class Transaction
57
+ # The id of this transaction, used to reference the transaction with the stomp broker.
58
+ attr_reader :id
59
+
60
+ # Creates a new Transaction instance. The +client+ parameter
61
+ # is an instance of Stomper::Client and is required so that the Transaction
62
+ # instance has somewhere to forward +begin+, +ack+ and +abort+ methods
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
65
+ # through the +id+ attribute and is used in naming the transaction to
66
+ # the stomp broker. If +block+ is given, the Transaction instance immediately
67
+ # calls its perform method with the supplied +block+.
68
+ def initialize(client, trans_id=nil, &block)
69
+ @client = client
70
+ @id = trans_id || "tx-#{Time.now.to_f}"
71
+ @committed = false
72
+ @aborted = false
73
+ perform(&block) if block_given?
74
+ end
75
+
76
+ # Invokes the given +block+. If the +block+ executes normally, the
77
+ # transaction is committed, otherwise it is aborted.
78
+ # If +block+ accepts a parameter, this method yields itself to the block,
79
+ # otherwise, +block+ is evaluated within the context of this instance through
80
+ # +instance_eval+.
81
+ #
82
+ # If a call to +abort+ is issued within the block, the transaction is aborted
83
+ # as demanded, and no attempt is made to commit it; however, no code after the
84
+ # call to +abort+ will be evaluated, as +abort+ raises a TransactionAborted
85
+ # exception.
86
+ #
87
+ # If a call to +commit+ is issued within the block, the transaction is committed
88
+ # as demanded, and no attempt is made to commit it after +block+ has finished
89
+ # executing. As +commit+ does not raise an excpetion, all code after the call
90
+ # to commit will be evaluated.
91
+ #
92
+ # If you are using Transaction objects directly, and not relying on their
93
+ # generation through Stomper::Client#transaction, be warned that this method
94
+ # will raise a TransactionAborted exception if the +block+ evaluation fails.
95
+ # This behavior allows for nesting transactions and ensuring that if a nested
96
+ # transaction fails, so do all of its ancestors.
97
+ def perform(&block) #:yields: transaction
98
+ begin
99
+ @client.begin(@id)
100
+ if block.arity == 1
101
+ yield self
102
+ else
103
+ instance_eval(&block)
104
+ end
105
+ commit
106
+ rescue => err
107
+ _abort
108
+ raise TransactionAborted, "aborted transaction '#{@id}' originator: #{err.to_s}"
109
+ end
110
+ end
111
+
112
+ # Returns true if the Transaction object has already been committed, false
113
+ # otherwise.
114
+ def committed?
115
+ @committed
116
+ end
117
+
118
+ # Returns true if the Transaction object has already been aborted, false
119
+ # otherwise.
120
+ def aborted?
121
+ @aborted
122
+ end
123
+
124
+ # Similar to Stomper::Client#transaction, this method creates a new
125
+ # Transaction object, nested inside of this one. To prevent name
126
+ # collisions, this method automatically generates a transaction id,
127
+ # if one is not specified, of the form "#{parent_transaction_id}-#{Time.now.to_f}.
128
+ def transaction(transaction_id=nil,&block)
129
+ # To get a transaction name guaranteed to not collide with this one
130
+ # we will supply an explicit id to the constructor unless an id was
131
+ # provided
132
+ transaction_id ||= "#{@id}-#{Time.now.to_f}"
133
+ self.class.new(@client, transaction_id, &block)
134
+ end
135
+
136
+ # Wraps the Stomper::Client#send method, injecting a "transaction" header
137
+ # into the +headers+ hash, thus informing the stomp broker that the message
138
+ # generated here is part of this transaction.
139
+ def send(destination, body, headers={})
140
+ headers['transaction'] = @id
141
+ @client.send(destination, body, headers)
142
+ end
143
+
144
+ # Wraps the Stomper::Client#ack method, injecting a "transaction" header
145
+ # into the +headers+ hash, thus informing the stomp broker that the message
146
+ # acknowledgement is part of this transaction.
147
+ def ack(message_or_id, headers={})
148
+ headers['transaction'] = @id
149
+ @client.ack(message_or_id, headers)
150
+ end
151
+
152
+ # Aborts this transaction if it has not already been committed or aborted.
153
+ # Note that it does so by raising a TransactionAborted exception, allowing
154
+ # the +abort+ call to force any ancestral transactions to also fail.
155
+ #
156
+ # See also: commit, committed?, aborted?
157
+ def abort
158
+ raise TransactionAborted, "transaction '#{@id}' aborted explicitly" if _abort
159
+ end
160
+
161
+ # Commits this transaction unless it has already been committed or aborted.
162
+ #
163
+ # See also: abort, committed?, aborted?
164
+ def commit
165
+ # Guard against sending multiple commit messages to the server for a
166
+ # single transaction.
167
+ @client.commit(@id) unless committed? || aborted?
168
+ @committed = true
169
+ end
170
+
171
+ private
172
+ def _abort
173
+ # Guard against sending multiple abort messages to the server for a
174
+ # single transaction.
175
+ return false if committed? || aborted?
176
+ @client.abort(@id)
177
+ @aborted = true
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,167 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
2
+
3
+ module Stomper
4
+ describe Client do
5
+ 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:///")
11
+ end
12
+
13
+ describe "expected interface" do
14
+ it "should provide a send method" do
15
+ @client.should respond_to(:send)
16
+ @mock_connection.should_receive(:transmit).with(an_instance_of(Stomper::Frames::Send)).twice.and_return(nil)
17
+ @client.send("/queue/to", "message body", {:additional => 'header'})
18
+ @client.send("/queue/to", "message body")
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
+ it "should provide an ack method" do
36
+ @client.should respond_to(:ack)
37
+ @mock_connection.should_receive(:transmit).with(an_instance_of(Stomper::Frames::Ack)).exactly(3).times.and_return(nil)
38
+ @client.ack("message-id", {:additional => "header"})
39
+ @client.ack("message-id")
40
+ @client.ack(Stomper::Frames::Message.new({:'message-id' => 'msg-001'}, "body"))
41
+ 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
+ end
70
+
71
+ describe "threaded receiver" do
72
+ it "should respond to start and stop" do
73
+ @client.should respond_to(:start)
74
+ @client.should respond_to(:stop)
75
+ @client.should respond_to(:receiving?)
76
+ end
77
+ it "should only be receiving when it is started" do
78
+ @mock_connection.stub!(:receive).and_return(nil)
79
+ @mock_connection.should_receive(:connected?).any_number_of_times.and_return(true)
80
+ @client.receiving?.should be_false
81
+ @client.start
82
+ @client.receiving?.should be_true
83
+ @client.stop
84
+ @client.receiving?.should be_false
85
+ end
86
+ end
87
+
88
+ describe "subscribing to queue" do
89
+ before(:each) do
90
+ @message_sent = Stomper::Frames::Message.new({'destination' => "/queue/test"}, "test message")
91
+ @mock_connection.should_receive(:connected?).any_number_of_times.and_return(true)
92
+ @mock_connection.should_receive(:transmit).with(duck_type(:to_stomp)).at_least(:once).and_return(nil)
93
+ @mock_connection.should_receive(:receive).any_number_of_times.and_return(@message_sent)
94
+ end
95
+
96
+ it "should subscribe to a destination with a block" do
97
+ wait_for_message = true
98
+ @message_received = nil
99
+ @client.start
100
+ @client.subscribe("/queue/test") do |msg|
101
+ @message_received = msg
102
+ wait_for_message = false
103
+ end
104
+ true while wait_for_message
105
+ @client.stop
106
+ @message_received.should == @message_sent
107
+ end
108
+
109
+ it "should not unsubscribe from all destinations when a subscription id is provided" do
110
+ @client.subscribe("/queue/test", { 'id' => 'subscription-1' }) do |msg|
111
+ @message_received = msg
112
+ end
113
+ @client.subscribe("/queue/test") do |msg|
114
+ @message_received = msg
115
+ end
116
+ @client.subscribe("/queue/test", :id => 'subscription-2') do |msg|
117
+ @message_received = msg
118
+ end
119
+ @client.unsubscribe("/queue/test", 'subscription-1')
120
+ @client.subscriptions.size.should == 2
121
+ end
122
+
123
+ it "should not unsubscribe from non-naive subscriptions when only a destination is supplied" do
124
+ @client.subscribe("/queue/test", { 'id' => 'subscription-1' }) do |msg|
125
+ @message_received = msg
126
+ end
127
+ @client.subscribe("/queue/test") do |msg|
128
+ @message_received = msg
129
+ end
130
+ @client.subscribe("/queue/test") do |msg|
131
+ @message_received = msg
132
+ end
133
+ @client.unsubscribe("/queue/test")
134
+ @client.subscriptions.size.should == 1
135
+ end
136
+
137
+ # Due to the receiver running in a separate thread, this may not be correct?
138
+ it "should unsubscribe from a destination and receive no more messages" do
139
+ @mutex = Mutex.new
140
+ @last_message_received = nil
141
+ @client.start
142
+ @client.subscribe("/queue/test") do |msg|
143
+ @last_message_received = Time.now
144
+ end
145
+ true until @last_message_received
146
+ @client.unsubscribe("/queue/test")
147
+ @unsubscribed_at = Time.now
148
+ @client.stop
149
+ (@last_message_received < @unsubscribed_at).should be_true
150
+ end
151
+ end
152
+
153
+ describe "transactions" do
154
+ before(:each) do
155
+ @mock_connection.should_receive(:transmit).with(duck_type(:to_stomp)).at_least(:once).and_return(nil)
156
+ end
157
+
158
+ it "should provide a transaction method that generates a new Transaction" do
159
+ @evaluated = false
160
+ @client.transaction do |t|
161
+ @evaluated = true
162
+ end
163
+ @evaluated.should be_true
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,12 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
2
+ require File.expand_path(File.join(File.dirname(__FILE__), 'shared_connection_examples'))
3
+
4
+ module Stomper
5
+ describe Connection do
6
+ before(:each) do
7
+ @connection = Connection.new("stomp:///")
8
+ end
9
+
10
+ it_should_behave_like "All Client Connections"
11
+ end
12
+ end
@@ -0,0 +1,142 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
2
+
3
+ module Stomper::Frames
4
+ describe ClientFrame do
5
+ before(:each) do
6
+ ClientFrame.generate_content_length = true
7
+ @client_frame = ClientFrame.new('COMMAND')
8
+ end
9
+
10
+ def str_size(str)
11
+ str.respond_to?(:bytesize) ? str.bytesize : str.size
12
+ end
13
+
14
+ it "should be provide a headers as an instance of Headers" do
15
+ @client_frame.headers.should be_an_instance_of(Stomper::Frames::Headers)
16
+ end
17
+
18
+ it "should be convertable into a stomp frame" do
19
+ @client_frame.to_stomp.should == "COMMAND\n\n\0"
20
+ @client_frame.headers.destination = "/queue/test/1"
21
+ @client_frame.headers['transaction-id'] = '2'
22
+ @client_frame.headers[:ack] = 'client'
23
+ @client_frame.to_stomp.should == "COMMAND\nack:client\ndestination:/queue/test/1\ntransaction-id:2\n\n\0"
24
+ end
25
+
26
+ describe "generating content-length header" do
27
+ it "should provide the header by default, overriding any existing header" do
28
+ @frame_body = 'testing'
29
+ @client_frame = ClientFrame.new('COMMAND', {'content-length' => 1}, @frame_body)
30
+ @client_frame.to_stomp.should == "COMMAND\ncontent-length:#{str_size(@frame_body)}\n\n#{@frame_body}\0"
31
+ end
32
+
33
+ it "should not provide the header if the class option is set to false, unless explicitly set on the frame in particular" do
34
+ ClientFrame.generate_content_length = false
35
+ @frame_body = 'testing'
36
+ @client_frame = ClientFrame.new('COMMAND', {}, @frame_body)
37
+ @client_frame.to_stomp.should == "COMMAND\n\n#{@frame_body}\0"
38
+ @client_frame = ClientFrame.new('COMMAND', {}, @frame_body)
39
+ @client_frame.generate_content_length = true
40
+ @client_frame.to_stomp.should == "COMMAND\ncontent-length:#{str_size(@frame_body)}\n\n#{@frame_body}\0"
41
+ end
42
+
43
+ it "should not provide the header if instance option is set false, when the class option is true" do
44
+ @frame_body = 'testing'
45
+ @client_frame = ClientFrame.new('COMMAND', {}, @frame_body)
46
+ @client_frame.generate_content_length = false
47
+ @client_frame.to_stomp.should == "COMMAND\n\n#{@frame_body}\0"
48
+ @client_frame = ClientFrame.new('COMMAND', {:generate_content_length => false}, @frame_body)
49
+ @client_frame.to_stomp.should == "COMMAND\n\n#{@frame_body}\0"
50
+ end
51
+
52
+ it "should not overwrite an explicit content-length header when option is off at class or instance level" do
53
+ @frame_body = 'testing'
54
+ @client_frame = ClientFrame.new('COMMAND', { 'content-length' => 4}, @frame_body)
55
+ @client_frame.generate_content_length = false
56
+ @client_frame.to_stomp.should == "COMMAND\ncontent-length:4\n\n#{@frame_body}\0"
57
+ ClientFrame.generate_content_length = false
58
+ @client_frame = ClientFrame.new('COMMAND', {'content-length' => 2}, @frame_body)
59
+ @client_frame.to_stomp.should == "COMMAND\ncontent-length:2\n\n#{@frame_body}\0"
60
+ end
61
+
62
+ it "should the class option should be scoped to the class it is set on" do
63
+ @frame_body = 'testing'
64
+ Send.generate_content_length = false
65
+ @send_frame = Send.new('/queue/test/1', @frame_body)
66
+ @client_frame = ClientFrame.new('COMMAND', {}, @frame_body)
67
+ @client_frame.to_stomp.should == "COMMAND\ncontent-length:#{str_size(@frame_body)}\n\n#{@frame_body}\0"
68
+ @send_frame.to_stomp.should == "SEND\ndestination:#{@send_frame.headers.destination}\n\n#{@frame_body}\0"
69
+ Send.generate_content_length = true
70
+ ClientFrame.generate_content_length = false
71
+ @send_frame = Send.new('/queue/test/1', @frame_body)
72
+ @client_frame = ClientFrame.new('COMMAND', {}, @frame_body)
73
+ @client_frame.to_stomp.should == "COMMAND\n\n#{@frame_body}\0"
74
+ @send_frame.to_stomp.should == "SEND\ncontent-length:#{str_size(@frame_body)}\ndestination:#{@send_frame.headers.destination}\n\n#{@frame_body}\0"
75
+ end
76
+ end
77
+ describe "client frames" do
78
+ describe Abort do
79
+ it "should produce a proper stomp message" do
80
+ @abort = Abort.new("transaction-test", { :a_header => 'test'})
81
+ @abort.to_stomp.should == "ABORT\na_header:test\ntransaction:transaction-test\n\n\0"
82
+ end
83
+ end
84
+ describe Ack do
85
+ it "should produce a proper stomp message" do
86
+ @ack = Ack.new("message-test", { :a_header => 'test'})
87
+ @ack.to_stomp.should == "ACK\na_header:test\nmessage-id:message-test\n\n\0"
88
+ end
89
+
90
+ it "should provide an Ack for a given message frame" do
91
+ @ack = Ack.ack_for(Message.new({'message-id' => 'test'}, "a body"))
92
+ @ack.to_stomp.should == "ACK\nmessage-id:test\n\n\0"
93
+ @ack = Ack.ack_for(Message.new({'message-id' => 'test', 'transaction' => 'tx-test'}, "a body"))
94
+ @ack.to_stomp.should == "ACK\nmessage-id:test\ntransaction:tx-test\n\n\0"
95
+ end
96
+
97
+ end
98
+ describe Begin do
99
+ it "should produce a proper stomp message" do
100
+ @begin = Begin.new("transaction-test", { :a_header => 'test'})
101
+ @begin.to_stomp.should == "BEGIN\na_header:test\ntransaction:transaction-test\n\n\0"
102
+ end
103
+ end
104
+ describe Commit do
105
+ it "should produce a proper stomp message" do
106
+ @commit = Commit.new("transaction-test", { :a_header => 'test'})
107
+ @commit.to_stomp.should == "COMMIT\na_header:test\ntransaction:transaction-test\n\n\0"
108
+ end
109
+ end
110
+ describe Connect do
111
+ it "should produce a proper stomp message" do
112
+ @connect = Connect.new('uzer','s3cr3t', { :a_header => 'test' })
113
+ @connect.to_stomp.should == "CONNECT\na_header:test\nlogin:uzer\npasscode:s3cr3t\n\n\0"
114
+ end
115
+ end
116
+ describe Disconnect do
117
+ it "should produce a proper stomp message" do
118
+ @disconnect = Disconnect.new({ :a_header => 'test'})
119
+ @disconnect.to_stomp.should == "DISCONNECT\na_header:test\n\n\0"
120
+ end
121
+ end
122
+ describe Send do
123
+ it "should produce a proper stomp message" do
124
+ @send = Send.new("/queue/a/target", "a body", { :a_header => 'test'})
125
+ @send.to_stomp.should == "SEND\na_header:test\ncontent-length:6\ndestination:/queue/a/target\n\na body\0"
126
+ end
127
+ end
128
+ describe Subscribe do
129
+ it "should produce a proper stomp message" do
130
+ @subscribe = Subscribe.new("/topic/some/target", { :a_header => 'test'})
131
+ @subscribe.to_stomp.should == "SUBSCRIBE\na_header:test\nack:auto\ndestination:/topic/some/target\n\n\0"
132
+ end
133
+ end
134
+ describe Unsubscribe do
135
+ it "should produce a proper stomp message" do
136
+ @unsubscribe = Unsubscribe.new("/topic/target.name.path", { :a_header => 'test'})
137
+ @unsubscribe.to_stomp.should == "UNSUBSCRIBE\na_header:test\ndestination:/topic/target.name.path\n\n\0"
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end