symphony 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -19,7 +19,7 @@ describe Symphony::Queue do
19
19
  it "can build a Hash of AMQP options from its configuration" do
20
20
  expect( described_class.amqp_session_options ).to include({
21
21
  heartbeat: :server,
22
- logger: Loggability[ Symphony ],
22
+ logger: Loggability[ Bunny ],
23
23
  })
24
24
  end
25
25
 
@@ -30,7 +30,7 @@ describe Symphony::Queue do
30
30
  host: 'spimethorpe.com',
31
31
  port: 23456,
32
32
  heartbeat: :server,
33
- logger: Loggability[ Symphony ],
33
+ logger: Loggability[ Bunny ],
34
34
  })
35
35
  end
36
36
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative '../helpers'
4
4
 
5
+ require 'ostruct'
5
6
  require 'symphony'
6
7
  require 'symphony/routing'
7
8
 
@@ -63,5 +64,301 @@ describe Symphony::Routing do
63
64
  ).to include( scheduled: '2 times an hour' )
64
65
  end
65
66
 
67
+
68
+ describe "route-matching" do
69
+
70
+ let( :task_class ) do
71
+ Class.new( Symphony::Task ) do
72
+ include Symphony::Routing
73
+ def initialize( * )
74
+ super
75
+ @run_data = Hash.new {|h,k| h[k] = [] }
76
+ end
77
+ attr_reader :run_data
78
+ end
79
+ end
80
+
81
+
82
+ ### Call the task's #work method with the same stuff Bunny would for the
83
+ ### given eventname
84
+ def publish( eventname )
85
+ delivery_info = OpenStruct.new( routing_key: eventname )
86
+ properties = OpenStruct.new( content_type: 'application/json' )
87
+
88
+ metadata = {
89
+ delivery_info: delivery_info,
90
+ properties: properties,
91
+ content_type: properties.content_type,
92
+ }
93
+
94
+ payload = '[]'
95
+ return task.work( payload, metadata )
96
+ end
97
+
98
+
99
+ context "for one-segment explicit routing keys (`simple`)" do
100
+
101
+ let( :task ) do
102
+ task_class.on( 'simple' ) {|*args| self.run_data[:simple] << args }
103
+ task_class.new( :queue )
104
+ end
105
+
106
+
107
+ it "runs the job for routing keys that exactly match" do
108
+ expect {
109
+ publish( 'simple' )
110
+ }.to change { task.run_data[:simple].length }.by( 1 )
111
+ end
112
+
113
+
114
+ it "doesn't run the job for routing keys which don't exactly match" do
115
+ expect {
116
+ publish( 'notsimple' )
117
+ }.to_not change { task.run_data[:simple].length }
118
+ end
119
+
120
+
121
+ it "doesn't run the job for routing keys which contain additional segments" do
122
+ expect {
123
+ publish( 'simple.additional1' )
124
+ }.to_not change { task.run_data[:simple_splat].length }
125
+ end
126
+
127
+ end
128
+
129
+
130
+ context "for routing keys with one segment wildcard (`simple.*`)" do
131
+
132
+ let( :task ) do
133
+ task_class.on( 'simple.*' ) {|*args| self.run_data[:simple_splat] << args }
134
+ task_class.new( :queue )
135
+ end
136
+
137
+
138
+ it "runs the job for routing keys with the same first segment and one additional segment" do
139
+ expect {
140
+ publish( 'simple.additional1' )
141
+ }.to change { task.run_data[:simple_splat].length }
142
+ end
143
+
144
+
145
+ it "doesn't run the job for routing keys with only the same first segment" do
146
+ expect {
147
+ publish( 'simple' )
148
+ }.to_not change { task.run_data[:simple_splat].length }
149
+ end
150
+
151
+
152
+ it "doesn't run the job for routing keys with a different first segment" do
153
+ expect {
154
+ publish( 'notsimple.additional1' )
155
+ }.to_not change { task.run_data[:simple_splat].length }
156
+ end
157
+
158
+
159
+ it "doesn't run the job for routing keys which contain two additional segments" do
160
+ expect {
161
+ publish( 'simple.additional1.additional2' )
162
+ }.to_not change { task.run_data[:simple_splat].length }
163
+ end
164
+
165
+
166
+ it "doesn't run the job for routing keys with a matching second segment" do
167
+ expect {
168
+ publish( 'prepended.simple.additional1' )
169
+ }.to_not change { task.run_data[:simple_splat].length }
170
+ end
171
+
172
+ end
173
+
174
+
175
+ context "for routing keys with two consecutive segment wildcards (`simple.*.*`)" do
176
+
177
+ let( :task ) do
178
+ task_class.on( 'simple.*.*' ) {|*args| self.run_data[:simple_splat_splat] << args }
179
+ task_class.new( :queue )
180
+ end
181
+
182
+
183
+ it "runs the job for routing keys which contain two additional segments" do
184
+ expect {
185
+ publish( 'simple.additional1.additional2' )
186
+ }.to change { task.run_data[:simple_splat_splat].length }
187
+ end
188
+
189
+
190
+ it "doesn't run the job for routing keys with the same first segment and one additional segment" do
191
+ expect {
192
+ publish( 'simple.additional1' )
193
+ }.to_not change { task.run_data[:simple_splat_splat].length }
194
+ end
195
+
196
+
197
+ it "doesn't run the job for routing keys with only the same first segment" do
198
+ expect {
199
+ publish( 'simple' )
200
+ }.to_not change { task.run_data[:simple_splat_splat].length }
201
+ end
202
+
203
+
204
+ it "doesn't run the job for routing keys with a different first segment" do
205
+ expect {
206
+ publish( 'notsimple.additional1' )
207
+ }.to_not change { task.run_data[:simple_splat_splat].length }
208
+ end
209
+
210
+
211
+ it "doesn't run the job for routing keys with a matching second segment" do
212
+ expect {
213
+ publish( 'prepended.simple.additional1' )
214
+ }.to_not change { task.run_data[:simple_splat_splat].length }
215
+ end
216
+
217
+ end
218
+
219
+
220
+ context "for routing keys with bracketing segment wildcards (`*.simple.*`)" do
221
+
222
+ let( :task ) do
223
+ task_class.on( '*.simple.*' ) {|*args| self.run_data[:splat_simple_splat] << args }
224
+ task_class.new( :queue )
225
+ end
226
+
227
+
228
+ it "runs the job for routing keys with a matching second segment" do
229
+ expect {
230
+ publish( 'prepended.simple.additional1' )
231
+ }.to change { task.run_data[:splat_simple_splat].length }
232
+ end
233
+
234
+
235
+ it "doesn't run the job for routing keys which contain two additional segments" do
236
+ expect {
237
+ publish( 'simple.additional1.additional2' )
238
+ }.to_not change { task.run_data[:splat_simple_splat].length }
239
+ end
240
+
241
+
242
+ it "doesn't run the job for routing keys with the same first segment and one additional segment" do
243
+ expect {
244
+ publish( 'simple.additional1' )
245
+ }.to_not change { task.run_data[:splat_simple_splat].length }
246
+ end
247
+
248
+
249
+ it "doesn't run the job for routing keys with only the same first segment" do
250
+ expect {
251
+ publish( 'simple' )
252
+ }.to_not change { task.run_data[:splat_simple_splat].length }
253
+ end
254
+
255
+
256
+ it "doesn't run the job for routing keys with a different first segment" do
257
+ expect {
258
+ publish( 'notsimple.additional1' )
259
+ }.to_not change { task.run_data[:splat_simple_splat].length }
260
+ end
261
+
262
+ end
263
+
264
+
265
+ context "for routing keys with a multi-segment wildcard (`simple.#`)" do
266
+
267
+ let( :task ) do
268
+ task_class.on( 'simple.#' ) {|*args| self.run_data[:simple_hash] << args }
269
+ task_class.new( :queue )
270
+ end
271
+
272
+
273
+ it "runs the job for routing keys with the same first segment and one additional segment" do
274
+ expect {
275
+ publish( 'simple.additional1' )
276
+ }.to change { task.run_data[:simple_hash].length }
277
+ end
278
+
279
+
280
+ it "runs the job for routing keys which contain two additional segments" do
281
+ expect {
282
+ publish( 'simple.additional1.additional2' )
283
+ }.to change { task.run_data[:simple_hash].length }
284
+ end
285
+
286
+
287
+ it "runs the job for routing keys with only the same first segment" do
288
+ expect {
289
+ publish( 'simple' )
290
+ }.to change { task.run_data[:simple_hash].length }
291
+ end
292
+
293
+
294
+ it "doesn't run the job for routing keys with a different first segment" do
295
+ expect {
296
+ publish( 'notsimple.additional1' )
297
+ }.to_not change { task.run_data[:simple_hash].length }
298
+ end
299
+
300
+
301
+ it "doesn't run the job for routing keys with a matching second segment" do
302
+ expect {
303
+ publish( 'prepended.simple.additional1' )
304
+ }.to_not change { task.run_data[:simple_hash].length }
305
+ end
306
+
307
+ end
308
+
309
+
310
+ context "for routing keys with bracketing multi-segment wildcards (`#.simple.#`)" do
311
+
312
+ let( :task ) do
313
+ task_class.on( '#.simple.#' ) {|*args| self.run_data[:hash_simple_hash] << args }
314
+ task_class.new( :queue )
315
+ end
316
+
317
+
318
+ it "runs the job for routing keys with the same first segment and one additional segment" do
319
+ expect {
320
+ publish( 'simple.additional1' )
321
+ }.to change { task.run_data[:hash_simple_hash].length }
322
+ end
323
+
324
+
325
+ it "runs the job for routing keys which contain two additional segments" do
326
+ expect {
327
+ publish( 'simple.additional1.additional2' )
328
+ }.to change { task.run_data[:hash_simple_hash].length }
329
+ end
330
+
331
+
332
+ it "runs the job for routing keys with only the same first segment" do
333
+ expect {
334
+ publish( 'simple' )
335
+ }.to change { task.run_data[:hash_simple_hash].length }
336
+ end
337
+
338
+
339
+ it "runs the job for three-segment routing keys with a matching second segment" do
340
+ expect {
341
+ publish( 'prepended.simple.additional1' )
342
+ }.to change { task.run_data[:hash_simple_hash].length }
343
+ end
344
+
345
+
346
+ it "runs the job for two-segment routing keys with a matching second segment" do
347
+ expect {
348
+ publish( 'prepended.simple' )
349
+ }.to change { task.run_data[:hash_simple_hash].length }
350
+ end
351
+
352
+
353
+ it "doesn't run the job for routing keys with a different first segment" do
354
+ expect {
355
+ publish( 'notsimple.additional1' )
356
+ }.to_not change { task.run_data[:hash_simple_hash].length }
357
+ end
358
+
359
+ end
360
+
361
+ end
362
+
66
363
  end
67
364
 
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../helpers'
4
+
5
+ require 'symphony/statistics'
6
+
7
+
8
+ describe Symphony::Statistics do
9
+
10
+
11
+ let( :including_class ) do
12
+ new_class = Class.new
13
+ new_class.instance_exec( described_class ) do |mixin|
14
+ include( mixin )
15
+ end
16
+ end
17
+
18
+ let( :object_with_statistics ) { including_class.new }
19
+
20
+
21
+ def make_samples( *counts )
22
+ start = 1414002605.0
23
+ offset = 0
24
+ return counts.each_with_object([]) do |count, accum|
25
+ accum << [ start + offset, count ]
26
+ offset += 1
27
+ end
28
+ end
29
+
30
+
31
+ it "can detect an upwards trend in a sample set" do
32
+ samples = make_samples( 1, 2, 2, 3, 3, 3, 4 )
33
+
34
+ object_with_statistics.sample_size = samples.size
35
+ object_with_statistics.samples.concat( samples )
36
+
37
+ expect( object_with_statistics.sample_values_increasing? ).to be_truthy
38
+ end
39
+
40
+
41
+ it "can detect a downwards trend in a sample set" do
42
+ samples = make_samples( 4, 3, 3, 2, 2, 2, 1 )
43
+
44
+ object_with_statistics.sample_size = samples.size
45
+ object_with_statistics.samples.concat( samples )
46
+
47
+ expect( object_with_statistics.sample_values_decreasing? ).to be_truthy
48
+ end
49
+
50
+
51
+ it "isn't fooled by transitory spikes" do
52
+ samples = make_samples( 1, 2, 222, 3, 2, 3, 2 )
53
+
54
+ object_with_statistics.sample_size = samples.size
55
+ object_with_statistics.samples.concat( samples )
56
+
57
+ expect( object_with_statistics.sample_values_increasing? ).to be_falsey
58
+ end
59
+
60
+
61
+ it "doesn't try to detect a trend with a sample set that's too small" do
62
+ upward_samples = make_samples( 1, 2, 2, 3, 3, 3, 4 )
63
+ object_with_statistics.samples.replace( upward_samples )
64
+ expect( object_with_statistics.sample_values_increasing? ).to be_falsey
65
+
66
+ downward_samples = make_samples( 4, 3, 3, 2, 2, 2, 1 )
67
+ object_with_statistics.samples.replace( downward_samples )
68
+ expect( object_with_statistics.sample_values_decreasing? ).to be_falsey
69
+ end
70
+ end
71
+
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env rspec -cfd
2
+
3
+ require_relative '../../helpers'
4
+
5
+ require 'symphony/task_group/longlived'
6
+
7
+ describe Symphony::TaskGroup::LongLived do
8
+
9
+ let( :task ) do
10
+ Class.new( Symphony::Task ) do
11
+ extend Symphony::MethodUtilities
12
+
13
+ singleton_attr_accessor :has_before_forked, :has_after_forked, :has_run
14
+
15
+ def self::before_fork
16
+ self.has_before_forked = true
17
+ end
18
+ def self::after_fork
19
+ self.has_after_forked = true
20
+ end
21
+ def self::run
22
+ self.has_run = true
23
+ end
24
+ end
25
+ end
26
+
27
+ let( :task_group ) do
28
+ described_class.new( task, 2 )
29
+ end
30
+
31
+
32
+ # not enough samples
33
+ # trending up
34
+
35
+
36
+
37
+ it "doesn't start anything if it's throttled" do
38
+ # Simulate a child starting up and failing
39
+ task_group.instance_variable_set( :@last_child_started, Time.now )
40
+ task_group.adjust_throttle( 5 )
41
+
42
+ expect( Process ).to_not receive( :fork )
43
+ expect( task_group.adjust_workers ).to be_nil
44
+ end
45
+
46
+
47
+ context "when told to adjust its worker pool" do
48
+
49
+ it "starts an initial worker if it doesn't have any" do
50
+ expect( Process ).to receive( :fork ).and_return( 414 )
51
+ allow( Process ).to receive( :setpgid ).with( 414, 0 )
52
+
53
+ channel = double( Bunny::Channel )
54
+ queue = double( Bunny::Queue )
55
+ expect( Symphony::Queue ).to receive( :amqp_channel ).
56
+ and_return( channel )
57
+ expect( channel ).to receive( :queue ).
58
+ with( task.queue_name, passive: true, prefetch: 0 ).
59
+ and_return( queue )
60
+
61
+ task_group.adjust_workers
62
+
63
+ expect( task_group.started_one_worker? ).to be_truthy
64
+ expect( task_group.pids ).to include( 414 )
65
+ end
66
+
67
+
68
+ it "starts an additional worker if its work load is trending upward" do
69
+ samples = [ 1, 2, 2, 3, 3, 3, 4 ]
70
+ task_group.sample_size = samples.size
71
+
72
+ expect( Process ).to receive( :fork ).and_return( 525, 528 )
73
+ allow( Process ).to receive( :setpgid )
74
+
75
+ channel = double( Bunny::Channel )
76
+ queue = double( Bunny::Queue )
77
+ expect( Symphony::Queue ).to receive( :amqp_channel ).
78
+ and_return( channel )
79
+ expect( channel ).to receive( :queue ).
80
+ with( task.queue_name, passive: true, prefetch: 0 ).
81
+ and_return( queue )
82
+
83
+ allow( queue ).to receive( :consumer_count ).and_return( 1 )
84
+ expect( queue ).to receive( :message_count ).and_return( *samples )
85
+
86
+ start = 1414002605
87
+ start.upto( start + samples.size ) do |time|
88
+ Timecop.freeze( time ) do
89
+ task_group.adjust_workers
90
+ end
91
+ end
92
+
93
+ expect( task_group.started_one_worker? ).to be_truthy
94
+ expect( task_group.pids ).to include( 525, 528 )
95
+ end
96
+
97
+
98
+ it "starts an additional worker if its work load is holding steady at a non-zero value" do
99
+ pending "this being a problem we see in practice"
100
+ samples = [ 4, 4, 4, 5, 5, 4, 4 ]
101
+ task_group.sample_size = samples.size
102
+
103
+ expect( Process ).to receive( :fork ).and_return( 525, 528 )
104
+ allow( Process ).to receive( :setpgid )
105
+
106
+ channel = double( Bunny::Channel )
107
+ queue = double( Bunny::Queue )
108
+ expect( Symphony::Queue ).to receive( :amqp_channel ).
109
+ and_return( channel )
110
+ expect( channel ).to receive( :queue ).
111
+ with( task.queue_name, passive: true, prefetch: 0 ).
112
+ and_return( queue )
113
+
114
+ allow( queue ).to receive( :consumer_count ).and_return( 1 )
115
+ expect( queue ).to receive( :message_count ).and_return( *samples )
116
+
117
+ start = 1414002605
118
+ start.upto( start + samples.size ) do |time|
119
+ Timecop.freeze( time ) do
120
+ task_group.adjust_workers
121
+ end
122
+ end
123
+
124
+ expect( task_group.pids.size ).to eq( 2 )
125
+ end
126
+
127
+
128
+ it "doesn't start a worker if it's already running the maximum number of workers" do
129
+ samples = [ 1, 2, 2, 3, 3, 3, 4, 4, 4, 5 ]
130
+ consumers = [ 1, 1, 1, 1, 1, 1, 1, 2, 2, 2 ]
131
+ task_group.sample_size = samples.size - 3
132
+
133
+ expect( Process ).to receive( :fork ).and_return( 525, 528 )
134
+ allow( Process ).to receive( :setpgid )
135
+
136
+ channel = double( Bunny::Channel )
137
+ queue = double( Bunny::Queue )
138
+ expect( Symphony::Queue ).to receive( :amqp_channel ).
139
+ and_return( channel )
140
+ expect( channel ).to receive( :queue ).
141
+ with( task.queue_name, passive: true, prefetch: 0 ).
142
+ and_return( queue )
143
+
144
+ expect( queue ).to receive( :consumer_count ).and_return( *consumers )
145
+ expect( queue ).to receive( :message_count ).and_return( *samples )
146
+
147
+ start = 1414002605
148
+ start.upto( start + samples.size ) do |time|
149
+ Timecop.freeze( time ) do
150
+ task_group.adjust_workers
151
+ end
152
+ end
153
+
154
+ expect( task_group.pids.size ).to eq( 2 )
155
+ end
156
+
157
+
158
+ it "doesn't start anything if its work load is holding steady at zero" do
159
+ samples = [ 0, 1, 0, 0, 0, 0, 1, 0, 0 ]
160
+ task_group.sample_size = samples.size - 3
161
+
162
+ expect( Process ).to receive( :fork ).and_return( 525 )
163
+ allow( Process ).to receive( :setpgid )
164
+
165
+ channel = double( Bunny::Channel )
166
+ queue = double( Bunny::Queue )
167
+ expect( Symphony::Queue ).to receive( :amqp_channel ).
168
+ and_return( channel )
169
+ expect( channel ).to receive( :queue ).
170
+ with( task.queue_name, passive: true, prefetch: 0 ).
171
+ and_return( queue )
172
+
173
+ allow( queue ).to receive( :consumer_count ).and_return( 1 )
174
+ expect( queue ).to receive( :message_count ).and_return( *samples )
175
+
176
+ start = 1414002605
177
+ start.upto( start + samples.size ) do |time|
178
+ Timecop.freeze( time ) do
179
+ task_group.adjust_workers
180
+ end
181
+ end
182
+
183
+ expect( task_group.pids.size ).to eq( 1 )
184
+ end
185
+
186
+
187
+ it "doesn't start anything if its work load is trending downward" do
188
+ samples = [ 4, 3, 3, 2, 2, 2, 1, 1, 0, 0 ]
189
+ task_group.sample_size = samples.size
190
+
191
+ expect( Process ).to receive( :fork ).and_return( 525 )
192
+ allow( Process ).to receive( :setpgid )
193
+
194
+ channel = double( Bunny::Channel )
195
+ queue = double( Bunny::Queue )
196
+ expect( Symphony::Queue ).to receive( :amqp_channel ).
197
+ and_return( channel )
198
+ expect( channel ).to receive( :queue ).
199
+ with( task.queue_name, passive: true, prefetch: 0 ).
200
+ and_return( queue )
201
+
202
+ allow( queue ).to receive( :consumer_count ).and_return( 1 )
203
+ expect( queue ).to receive( :message_count ).and_return( *samples )
204
+
205
+ start = 1414002605
206
+ start.upto( start + samples.size ) do |time|
207
+ Timecop.freeze( time ) do
208
+ task_group.adjust_workers
209
+ end
210
+ end
211
+
212
+ expect( task_group.started_one_worker? ).to be_truthy
213
+ expect( task_group.pids.size ).to eq( 1 )
214
+ end
215
+
216
+ end
217
+
218
+ end
219
+