symphony 0.3.0.pre20140327204419

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.
@@ -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