symphony 0.3.0.pre20140327204419

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,368 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../helpers'
4
+
5
+ require 'symphony/queue'
6
+
7
+ describe Symphony::Queue do
8
+
9
+
10
+ before( :each ) do
11
+ described_class.configure( broker_uri: 'amqp://example.com/%2Ftesty' )
12
+ described_class.reset
13
+ end
14
+
15
+
16
+ it_should_behave_like "an object with Configurability"
17
+
18
+
19
+ it "can build a Hash of AMQP options from its configuration" do
20
+ expect( described_class.amqp_session_options ).to include({
21
+ heartbeat: :server,
22
+ logger: Loggability[ Symphony ],
23
+ })
24
+ end
25
+
26
+
27
+ it "can use the Bunny-style configuration Hash" do
28
+ described_class.configure( host: 'spimethorpe.com', port: 23456 )
29
+ expect( described_class.amqp_session_options ).to include({
30
+ host: 'spimethorpe.com',
31
+ port: 23456,
32
+ heartbeat: :server,
33
+ logger: Loggability[ Symphony ],
34
+ })
35
+ end
36
+
37
+
38
+ it "assumes Bunny-style configuration Hash if no broker uri is configured" do
39
+ described_class.configure( host: 'spimethorpe.com', port: 23456 )
40
+ described_class.broker_uri = nil
41
+
42
+ expect( Bunny ).to receive( :new ).
43
+ with( described_class.amqp_session_options )
44
+
45
+ described_class.amqp_session
46
+ end
47
+
48
+
49
+ context "bunny interaction" do
50
+
51
+
52
+ it "can build a new Bunny session using the loaded configuration" do
53
+ clowney = double( "Bunny session" )
54
+ expect( Bunny::Session ).to receive( :new ).
55
+ with( described_class.broker_uri, described_class.amqp_session_options ).
56
+ and_return( clowney )
57
+
58
+ expect( described_class.amqp_session ).to be( clowney )
59
+ end
60
+
61
+ it "doesn't recreate the bunny session across multiple calls" do
62
+ bunny = double( "Bunny session" )
63
+ expect( Bunny::Session ).to receive( :new ).
64
+ once.
65
+ with( described_class.broker_uri, described_class.amqp_session_options ).
66
+ and_return( bunny )
67
+
68
+ expect( described_class.amqp_session ).to be( bunny )
69
+ expect( described_class.amqp_session ).to be( bunny )
70
+ end
71
+
72
+ it "can open a channel on the Bunny session" do
73
+ bunny = double( "Bunny session" )
74
+ channel = double( "Bunny channel" )
75
+ expect( Bunny ).to receive( :new ).
76
+ with( described_class.broker_uri, described_class.amqp_session_options ).
77
+ and_return( bunny )
78
+ expect( bunny ).to receive( :start )
79
+ expect( bunny ).to receive( :create_channel ).and_return( channel )
80
+
81
+ expect( described_class.amqp_channel ).to be( channel )
82
+ end
83
+
84
+ it "can fetch the configured exchange" do
85
+ bunny = double( "Bunny session" )
86
+ channel = double( "Bunny channel" )
87
+ exchange = double( "Symphony exchange" )
88
+ expect( Bunny ).to receive( :new ).
89
+ with( described_class.broker_uri, described_class.amqp_session_options ).
90
+ and_return( bunny )
91
+ expect( bunny ).to receive( :start )
92
+ expect( bunny ).to receive( :create_channel ).and_return( channel )
93
+ expect( channel ).to receive( :topic ).with( described_class.exchange, passive: true ).
94
+ and_return( exchange )
95
+
96
+ expect( described_class.amqp_exchange ).to be( exchange )
97
+ end
98
+ end
99
+
100
+
101
+ context "instance" do
102
+
103
+ let( :queue ) { described_class.for_task(testing_task_class) }
104
+
105
+ let( :testing_task_class ) { Class.new(Symphony::Task) }
106
+ let( :session ) { double("Bunny session", :start => true ) }
107
+
108
+ before( :each ) do
109
+ allow( Bunny ).to receive( :new ).and_return( session )
110
+ described_class.amqp[:exchange] = double( "AMQP exchange", name: 'the_exchange' )
111
+ described_class.amqp[:channel] = double( "AMQP channel" )
112
+ end
113
+
114
+
115
+ it "creates an auto-deleted queue for the task if one doesn't already exist" do
116
+ testing_task_class.subscribe_to( 'floppy.rabbit.#' )
117
+ expect( described_class.amqp_channel ).to receive( :queue ).
118
+ with( queue.name, passive: true ).
119
+ and_raise( Bunny::NotFound.new("no such queue", described_class.amqp_channel, true) )
120
+ expect( described_class.amqp_channel ).to receive( :open? ).
121
+ and_return( false )
122
+
123
+ # Channel is reset after queue creation fails
124
+ new_channel = double( "New AMQP channel" )
125
+ amqp_queue = double( "AMQP queue" )
126
+ allow( described_class.amqp_session ).to receive( :create_channel ).
127
+ and_return( new_channel )
128
+ expect( new_channel ).to receive( :prefetch ).
129
+ with( Symphony::Queue::DEFAULT_PREFETCH )
130
+ expect( new_channel ).to receive( :queue ).
131
+ with( queue.name, auto_delete: true ).
132
+ and_return( amqp_queue )
133
+ expect( amqp_queue ).to receive( :bind ).
134
+ with( described_class.amqp_exchange, routing_key: 'floppy.rabbit.#' )
135
+
136
+ expect( queue.create_amqp_queue ).to be( amqp_queue )
137
+ end
138
+
139
+
140
+ it "re-uses the existing queue on the broker if it already exists" do
141
+ amqp_queue = double( "AMQP queue" )
142
+ expect( described_class.amqp_channel ).to receive( :queue ).
143
+ with( queue.name, passive: true ).
144
+ and_return( amqp_queue )
145
+ expect( described_class.amqp_channel ).to receive( :prefetch ).
146
+ with( Symphony::Queue::DEFAULT_PREFETCH )
147
+
148
+ expect( queue.create_amqp_queue ).to be( amqp_queue )
149
+ end
150
+
151
+
152
+ it "subscribes to the message queue with a configured consumer to wait for messages" do
153
+ amqp_queue = double( "AMQP queue", channel: described_class.amqp_channel )
154
+ consumer = double( "Bunny consumer", channel: described_class.amqp_channel )
155
+
156
+ expect( described_class.amqp_channel ).to receive( :queue ).
157
+ with( testing_task_class.queue_name, passive: true ).
158
+ and_return( amqp_queue )
159
+ expect( described_class.amqp_channel ).to receive( :prefetch ).
160
+ with( Symphony::Queue::DEFAULT_PREFETCH )
161
+
162
+ expect( Bunny::Consumer ).to receive( :new ).
163
+ with( described_class.amqp_channel, amqp_queue, queue.consumer_tag, false ).
164
+ and_return( consumer )
165
+
166
+ # Set up an artificial method to call the delivery callback that we can later
167
+ # call ourselves
168
+ expect( consumer ).to receive( :on_delivery ) do |&block|
169
+ allow( consumer ).to receive( :deliver ) do
170
+ delivery_info = double("delivery info", delivery_tag: 'mirrors!!!!' )
171
+ properties = {:content_type => 'application/json'}
172
+ payload = '{"some": "stuff"}'
173
+ block.call( delivery_info, properties, payload )
174
+ end
175
+ end
176
+ expect( consumer ).to receive( :on_cancellation )
177
+
178
+ # When the queue subscription happens, call the hook we set up above to simulate
179
+ # the delivery of AMQP messages
180
+ expect( amqp_queue ).to receive( :subscribe_with ) do |*args|
181
+ expect( args.first ).to be( consumer )
182
+ expect( args.last ).to eq({ block: true })
183
+ 5.times { consumer.deliver }
184
+ end
185
+
186
+ expect( described_class.amqp_channel ).to receive( :acknowledge ).
187
+ with( 'mirrors!!!!' ).
188
+ exactly( 5 ).times
189
+ expect( described_class.amqp_channel ).to receive( :close )
190
+ expect( session ).to receive( :closed? ).and_return( false ).exactly( 5 ).times
191
+ expect( session ).to receive( :close )
192
+
193
+ count = 0
194
+ queue.wait_for_message { count += 1 }
195
+ expect( count ).to eq( 5 )
196
+ end
197
+
198
+
199
+ it "raises if wait_for_message is called without a block" do
200
+ expect { queue.wait_for_message }.to raise_error( LocalJumpError, /no work/i )
201
+ end
202
+
203
+
204
+ it "sets up the queue and consumer to only run once if waiting in one-shot mode" do
205
+ amqp_queue = double( "AMQP queue", channel: described_class.amqp_channel )
206
+ consumer = double( "Bunny consumer", channel: described_class.amqp_channel )
207
+
208
+ expect( described_class.amqp_channel ).to receive( :queue ).
209
+ with( testing_task_class.queue_name, passive: true ).
210
+ and_return( amqp_queue )
211
+ expect( described_class.amqp_channel ).to receive( :prefetch ).with( 1 )
212
+
213
+ expect( Bunny::Consumer ).to receive( :new ).
214
+ with( described_class.amqp_channel, amqp_queue, queue.consumer_tag, false ).
215
+ and_return( consumer )
216
+
217
+ expect( consumer ).to receive( :on_delivery ) do |&block|
218
+ allow( consumer ).to receive( :deliver ) do
219
+ delivery_info = double("delivery info", delivery_tag: 'mirrors!!!!' )
220
+ properties = {:content_type => 'application/json'}
221
+ payload = '{"some": "stuff"}'
222
+ block.call( delivery_info, properties, payload )
223
+ end
224
+ end
225
+ expect( consumer ).to receive( :on_cancellation )
226
+
227
+ expect( amqp_queue ).to receive( :subscribe_with ) do |*args|
228
+ expect( args.first ).to be( consumer )
229
+ expect( args.last ).to eq({ block: true })
230
+ consumer.deliver
231
+ end
232
+ expect( described_class.amqp_channel ).to receive( :acknowledge ).
233
+ with( 'mirrors!!!!' ).once
234
+ expect( described_class.amqp_channel ).to receive( :close )
235
+ expect( session ).to receive( :closed? ).and_return( false ).once
236
+ expect( session ).to receive( :close )
237
+ expect( consumer ).to receive( :cancel )
238
+
239
+ count = 0
240
+ queue.wait_for_message( true ) { count += 1 }
241
+ expect( count ).to eq( 1 )
242
+ end
243
+
244
+
245
+ it "shuts down the consumer if the queues it's consuming from is deleted on the server" do
246
+ amqp_queue = double( "AMQP queue", channel: described_class.amqp_channel )
247
+ consumer = double( "Bunny consumer", channel: described_class.amqp_channel )
248
+
249
+ expect( described_class.amqp_channel ).to receive( :queue ).
250
+ with( testing_task_class.queue_name, passive: true ).
251
+ and_return( amqp_queue )
252
+ expect( described_class.amqp_channel ).to receive( :prefetch ).
253
+ with( Symphony::Queue::DEFAULT_PREFETCH )
254
+
255
+ expect( Bunny::Consumer ).to receive( :new ).
256
+ with( described_class.amqp_channel, amqp_queue, queue.consumer_tag, false ).
257
+ and_return( consumer )
258
+
259
+ expect( consumer ).to receive( :on_delivery )
260
+ expect( consumer ).to receive( :on_cancellation ) do |&block|
261
+ allow( consumer ).to receive( :server_cancel ) do
262
+ block.call
263
+ end
264
+ end
265
+
266
+ expect( amqp_queue ).to receive( :subscribe_with ) do |*|
267
+ consumer.server_cancel
268
+ end
269
+ expect( described_class.amqp_channel ).to receive( :close )
270
+ expect( session ).to receive( :close )
271
+ expect( consumer ).to receive( :cancel )
272
+
273
+ queue.wait_for_message {}
274
+ expect( queue ).to be_shutting_down()
275
+ end
276
+
277
+
278
+ it "creates a consumer with acknowledgements enabled if it has acknowledgements enabled" do
279
+ amqp_channel = double( "AMQP channel" )
280
+ amqp_queue = double( "AMQP queue", channel: amqp_channel )
281
+ consumer = double( "Bunny consumer" )
282
+
283
+ # Ackmode argument is actually 'no_ack'
284
+ expect( Bunny::Consumer ).to receive( :new ).
285
+ with( amqp_channel, amqp_queue, queue.consumer_tag, false ).
286
+ and_return( consumer )
287
+ expect( consumer ).to receive( :on_delivery )
288
+ expect( consumer ).to receive( :on_cancellation )
289
+
290
+ expect( queue.create_consumer(amqp_queue) ).to be( consumer )
291
+ end
292
+
293
+
294
+ it "creates a consumer with acknowledgements disabled if it has acknowledgements disabled" do
295
+ amqp_channel = double( "AMQP channel" )
296
+ amqp_queue = double( "AMQP queue", channel: amqp_channel )
297
+ consumer = double( "Bunny consumer" )
298
+
299
+ # Ackmode argument is actually 'no_ack'
300
+ queue.instance_variable_set( :@acknowledge, false )
301
+ expect( Bunny::Consumer ).to receive( :new ).
302
+ with( amqp_channel, amqp_queue, queue.consumer_tag, true ).
303
+ and_return( consumer )
304
+ expect( consumer ).to receive( :on_delivery )
305
+ expect( consumer ).to receive( :on_cancellation )
306
+
307
+ expect( queue.create_consumer(amqp_queue) ).to be( consumer )
308
+ end
309
+
310
+
311
+ it "it acknowledges the message if acknowledgements are set and the task returns a true value" do
312
+ channel = double( "amqp channel" )
313
+ queue.consumer = double( "bunny consumer", channel: channel )
314
+ delivery_info = double( "delivery info", delivery_tag: 128 )
315
+
316
+ expect( channel ).to receive( :acknowledge ).with( delivery_info.delivery_tag )
317
+
318
+ queue.handle_message( delivery_info, {content_type: 'text/plain'}, :payload ) do |*|
319
+ true
320
+ end
321
+ end
322
+
323
+
324
+ it "re-raises AMQP errors raised while handling a message" do
325
+ channel = double( "amqp channel" )
326
+ queue.consumer = double( "bunny consumer", channel: channel )
327
+ delivery_info = double( "delivery info", delivery_tag: 128 )
328
+
329
+ expect( channel ).to_not receive( :acknowledge )
330
+
331
+ expect {
332
+ queue.handle_message( delivery_info, {content_type: 'text/plain'}, :payload ) do |*|
333
+ raise Bunny::Exception, 'something bad!'
334
+ end
335
+ }.to raise_error( Bunny::Exception, 'something bad!' )
336
+ end
337
+
338
+
339
+ it "it rejects the message if acknowledgements are set and the task returns a false value" do
340
+ channel = double( "amqp channel" )
341
+ queue.consumer = double( "bunny consumer", channel: channel )
342
+ delivery_info = double( "delivery info", delivery_tag: 128 )
343
+
344
+ expect( channel ).to receive( :reject ).with( delivery_info.delivery_tag, true )
345
+
346
+ queue.handle_message( delivery_info, {content_type: 'text/plain'}, :payload ) do |*|
347
+ false
348
+ end
349
+ end
350
+
351
+
352
+ it "it permanently rejects the message if acknowledgements are set and the task raises" do
353
+ channel = double( "amqp channel" )
354
+ queue.consumer = double( "bunny consumer", channel: channel )
355
+ delivery_info = double( "delivery info", delivery_tag: 128 )
356
+
357
+ expect( channel ).to receive( :reject ).with( delivery_info.delivery_tag, false )
358
+
359
+ queue.handle_message( delivery_info, {content_type: 'text/plain'}, :payload ) do |*|
360
+ raise "Uh-oh!"
361
+ end
362
+ end
363
+
364
+
365
+ end
366
+
367
+ end
368
+
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../helpers'
4
+
5
+ require 'symphony/task'
6
+
7
+ describe Symphony::Task do
8
+
9
+ before( :all ) do
10
+ Symphony::Queue.configure
11
+ end
12
+
13
+ before( :each ) do
14
+ Symphony::Queue.reset
15
+ allow( Bunny ).to receive( :new ).and_return( amqp_session )
16
+ end
17
+
18
+ after( :each ) do
19
+ # reset signal handlers
20
+ Symphony::Task::SIGNALS.each do |sig|
21
+ Signal.trap( sig, :DFL )
22
+ end
23
+ end
24
+
25
+
26
+ let( :amqp_session ) { double('amqp_session') }
27
+ let( :channel ) { double('amqp channel') }
28
+ let( :consumer ) { double('bunny consumer', channel: channel) }
29
+
30
+
31
+ it "cancels the AMQP consumer when it receives a TERM signal" do
32
+ queue = described_class.queue
33
+ queue.consumer = consumer
34
+ task = described_class.new( queue )
35
+
36
+ expect( queue.consumer ).to receive( :cancel )
37
+
38
+ task.handle_signal( :TERM )
39
+ end
40
+
41
+
42
+ it "cancels the AMQP consumer when it receives an INT signal" do
43
+ queue = described_class.queue
44
+ queue.consumer = consumer
45
+ task = described_class.new( queue )
46
+
47
+ expect( queue.consumer ).to receive( :cancel )
48
+
49
+ task.handle_signal( :INT )
50
+ end
51
+
52
+
53
+ it "closes the AMQP session when it receives a second TERM signal" do
54
+ queue = described_class.queue
55
+ queue.consumer = consumer
56
+ task = described_class.new( queue )
57
+ task.shutting_down = true
58
+
59
+ expect( queue.consumer.channel ).to receive( :close )
60
+
61
+ task.handle_signal( :TERM )
62
+ end
63
+
64
+
65
+ context "a concrete subclass" do
66
+
67
+ before( :each ) do
68
+ @task_class = Class.new( described_class ) do
69
+ def self::name; 'ACME::TestingTask'; end
70
+ end
71
+ end
72
+
73
+
74
+ it "raises an exception if run without specifying any subscriptions" do
75
+ expect { @task_class.run }.to raise_error( ScriptError, /no subscriptions/i )
76
+ end
77
+
78
+
79
+ it "can set an explicit queue name" do
80
+ @task_class.queue_name( 'happy.fun.queue' )
81
+ expect( @task_class.queue_name ).to eq( 'happy.fun.queue' )
82
+ end
83
+
84
+
85
+ it "can retry on timeout instead of rejecting" do
86
+ @task_class.timeout_action( :retry )
87
+ expect( @task_class.timeout_action ).to eq( :retry )
88
+ end
89
+
90
+
91
+ it "provides a default name for its queue based on its name" do
92
+ expect( @task_class.queue_name ).to eq( 'acme.testingtask' )
93
+ end
94
+
95
+
96
+ it "can declare a pattern to use when subscribing" do
97
+ @task_class.subscribe_to( 'foo.test' )
98
+ expect( @task_class.routing_keys ).to include( 'foo.test' )
99
+ end
100
+
101
+
102
+ it "has acknowledgements enabled by default" do
103
+ expect( @task_class.acknowledge ).to eq( true )
104
+ end
105
+
106
+
107
+ it "can enable acknowledgements" do
108
+ @task_class.acknowledge( true )
109
+ expect( @task_class.acknowledge ).to eq( true )
110
+ end
111
+
112
+
113
+ it "can disable acknowledgements" do
114
+ @task_class.acknowledge( false )
115
+ expect( @task_class.acknowledge ).to eq( false )
116
+ end
117
+
118
+
119
+ it "can set a timeout" do
120
+ @task_class.timeout( 10 )
121
+ expect( @task_class.timeout ).to eq( 10 )
122
+ end
123
+
124
+
125
+ it "can declare a one-shot work model" do
126
+ @task_class.work_model( :oneshot )
127
+ expect( @task_class.work_model ).to eq( :oneshot )
128
+ end
129
+
130
+
131
+ it "can declare a long-lived work model" do
132
+ @task_class.work_model( :longlived )
133
+ expect( @task_class.work_model ).to eq( :longlived )
134
+ end
135
+
136
+
137
+ it "raises an error if an invalid work model is declared " do
138
+ expect {
139
+ @task_class.work_model( :lazy )
140
+ }.to raise_error( /unknown work_model/i )
141
+ end
142
+
143
+
144
+ end
145
+
146
+ end
147
+
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env rspec
2
+
3
+ require_relative 'helpers'
4
+
5
+ require 'rspec'
6
+ require 'symphony'
7
+
8
+
9
+ describe Symphony do
10
+
11
+
12
+
13
+ end
14
+
data.tar.gz.sig ADDED
Binary file