symphony 0.8.0 → 0.9.0

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