distribot 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +13 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +3 -0
  5. data/.travis.yml +10 -0
  6. data/Dockerfile +9 -0
  7. data/Gemfile +6 -0
  8. data/Gemfile.lock +153 -0
  9. data/LICENSE +201 -0
  10. data/README.md +107 -0
  11. data/Rakefile +16 -0
  12. data/bin/distribot.flow-created +6 -0
  13. data/bin/distribot.flow-finished +6 -0
  14. data/bin/distribot.handler-finished +6 -0
  15. data/bin/distribot.phase-finished +6 -0
  16. data/bin/distribot.phase-started +6 -0
  17. data/bin/distribot.task-finished +6 -0
  18. data/distribot.gemspec +35 -0
  19. data/docker-compose.yml +29 -0
  20. data/examples/controller +168 -0
  21. data/examples/distribot.eye +49 -0
  22. data/examples/status +38 -0
  23. data/examples/worker +135 -0
  24. data/lib/distribot/connector.rb +162 -0
  25. data/lib/distribot/flow.rb +200 -0
  26. data/lib/distribot/flow_created_handler.rb +12 -0
  27. data/lib/distribot/flow_finished_handler.rb +12 -0
  28. data/lib/distribot/handler.rb +40 -0
  29. data/lib/distribot/handler_finished_handler.rb +29 -0
  30. data/lib/distribot/phase.rb +46 -0
  31. data/lib/distribot/phase_finished_handler.rb +19 -0
  32. data/lib/distribot/phase_handler.rb +15 -0
  33. data/lib/distribot/phase_started_handler.rb +69 -0
  34. data/lib/distribot/task_finished_handler.rb +37 -0
  35. data/lib/distribot/worker.rb +148 -0
  36. data/lib/distribot.rb +108 -0
  37. data/provision/nodes.sh +80 -0
  38. data/provision/templates/fluentd.conf +27 -0
  39. data/spec/distribot/bunny_connector_spec.rb +196 -0
  40. data/spec/distribot/connection_sharer_spec.rb +34 -0
  41. data/spec/distribot/connector_spec.rb +63 -0
  42. data/spec/distribot/flow_created_handler_spec.rb +32 -0
  43. data/spec/distribot/flow_finished_handler_spec.rb +32 -0
  44. data/spec/distribot/flow_spec.rb +661 -0
  45. data/spec/distribot/handler_finished_handler_spec.rb +112 -0
  46. data/spec/distribot/handler_spec.rb +32 -0
  47. data/spec/distribot/module_spec.rb +163 -0
  48. data/spec/distribot/multi_subscription_spec.rb +37 -0
  49. data/spec/distribot/phase_finished_handler_spec.rb +61 -0
  50. data/spec/distribot/phase_started_handler_spec.rb +150 -0
  51. data/spec/distribot/subscription_spec.rb +40 -0
  52. data/spec/distribot/task_finished_handler_spec.rb +71 -0
  53. data/spec/distribot/worker_spec.rb +281 -0
  54. data/spec/fixtures/simple_flow.json +49 -0
  55. data/spec/spec_helper.rb +74 -0
  56. metadata +371 -0
@@ -0,0 +1,281 @@
1
+ require 'spec_helper'
2
+
3
+ describe Distribot::Worker do
4
+ describe '.included(klass)' do
5
+ before do
6
+ @klass = "Foo#{SecureRandom.hex(10)}"
7
+ eval <<-EOF
8
+ class #{@klass}
9
+ include Distribot::Worker
10
+ version '1.1.1'
11
+ end
12
+ EOF
13
+ end
14
+ it 'adds an enumerate_with(:callback) method' do
15
+ expect(Kernel.const_get(@klass)).to respond_to(:enumerate_with)
16
+ end
17
+ it 'adds an enumerator accessor' do
18
+ Kernel.const_get(@klass).send :enumerate_with, 'foo'
19
+ expect(Kernel.const_get(@klass).send :enumerator).to eq 'foo'
20
+ end
21
+ it 'adds a process_tasks_with(:callback) method' do
22
+ expect(Kernel.const_get(@klass)).to respond_to(:process_tasks_with)
23
+ end
24
+ it 'adds a processor accessor' do
25
+ Kernel.const_get(@klass).send :process_tasks_with, 'foo'
26
+ expect(Kernel.const_get(@klass).send :processor).to eq 'foo'
27
+ end
28
+ it 'adds an enumeration_queue accessor' do
29
+ @klass_ref = Kernel.const_get(@klass)
30
+ expect(@klass_ref.send :enumeration_queue).to eq "distribot.flow.handler.#{@klass}.#{@klass_ref.version}.enumerate"
31
+ end
32
+ it 'adds a task_queue accessor' do
33
+ @klass_ref = Kernel.const_get(@klass)
34
+ expect(@klass_ref.send :task_queue).to eq "distribot.flow.handler.#{@klass}.#{@klass_ref.version}.tasks"
35
+ end
36
+ end
37
+
38
+ describe '#run' do
39
+ before :each do
40
+ @klass = "FooWorker#{SecureRandom.hex(8)}"
41
+ eval <<-EOF
42
+ class #{@klass}
43
+ include Distribot::Worker
44
+ enumerate_with :enumerate
45
+ process_tasks_with :process
46
+
47
+ def enumerate(context)
48
+ logger.info "HELLO FROM #{self}!"
49
+ jobs = [{id: 'job1'}, {id: 'job2'}]
50
+ return jobs
51
+ end
52
+
53
+ def process(context, job)
54
+ job
55
+ end
56
+ end
57
+ EOF
58
+ @class_ref = Kernel.const_get(@klass)
59
+ end
60
+ it 'prepares the worker' do
61
+ worker = @class_ref.new
62
+ expect(worker).to receive(:prepare_for_enumeration)
63
+ expect(worker).to receive(:subscribe_to_task_queue)
64
+ worker.run
65
+ end
66
+ describe '#prepare_for_enumeration' do
67
+ before do
68
+ @worker = @class_ref.new
69
+ @flow = Distribot::Flow.new(id: 'xxx', phases: [{name: 'start', is_initial: true}])
70
+ expect(Concurrent::FixedThreadPool).to receive(:new) do
71
+ pool = double('pool')
72
+ expect(pool).to receive(:post) do |&block|
73
+ block.call
74
+ end
75
+ pool
76
+ end
77
+ end
78
+ context 'when enumeration' do
79
+ context 'succeeds' do
80
+ it 'goes smoothly' do
81
+ message = {
82
+ flow_id: @flow.id,
83
+ phase: 'phase1',
84
+ task_queue: 'task-queue',
85
+ finished_queue: 'finished-queue',
86
+ handler: @klass
87
+ }
88
+ expect(Distribot).to receive(:subscribe).with(@class_ref.enumeration_queue, solo: true) do |&block|
89
+ @callback = block
90
+ end
91
+
92
+ expect(@worker).to receive(:enumerate_tasks).with(message)
93
+ expect(@worker).to receive(:announce_tasks).with(anything, message, anything)
94
+
95
+ # Finally:
96
+ @worker.prepare_for_enumeration
97
+
98
+ @callback.call(message)
99
+ end
100
+ end
101
+ context 'raises an exception' do
102
+ before do
103
+ expect(@worker).to receive(:enumerate_tasks){ raise "Test Error" }
104
+ expect(Distribot).to receive(:subscribe).with(@class_ref.enumeration_queue, solo: true) do |&block|
105
+ @callback = block
106
+ end
107
+ expect(@worker).to receive(:warn)
108
+ end
109
+ it 'logs the error and re-raises the exception' do
110
+ @worker.prepare_for_enumeration
111
+ expect{@callback.call({})}.to raise_error StandardError
112
+ end
113
+ end
114
+ end
115
+ end
116
+ describe '#enumerate_tasks(message)' do
117
+ before do
118
+ @klass_ref = Kernel.const_get(@klass)
119
+ @worker = @klass_ref.new
120
+ end
121
+ context 'when the flow' do
122
+ before do
123
+ @flow = double('flow')
124
+ expect(Distribot::Flow).to receive(:find).with( 'xxx' ){ @flow }
125
+ end
126
+ context 'is canceled' do
127
+ before do
128
+ expect(@flow).to receive(:canceled?){ true }
129
+ end
130
+ it 'raises an error' do
131
+ expect(@worker).to receive(:warn)
132
+ expect{@worker.enumerate_tasks(flow_id: 'xxx')}.to raise_error Distribot::FlowCanceledError
133
+ end
134
+ end
135
+ context 'is not canceled' do
136
+ before do
137
+ expect(@flow).to receive(:canceled?){ false }
138
+ end
139
+ it 'calls the task enumerator method, then accounts for the tasks it returns' do
140
+
141
+ # Finally:
142
+ @worker.enumerate_tasks(flow_id: 'xxx', task_counter: 'task.counter' )
143
+ end
144
+ end
145
+ end
146
+ end
147
+ describe '#announce_tasks(context, message, tasks)' do
148
+ before do
149
+ @flow = Distribot::Flow.new(id: 'xxx')
150
+ @message = {
151
+ flow_id: @flow.id,
152
+ phase: 'phase1',
153
+ task_queue: 'task-queue',
154
+ finished_queue: 'finished-queue',
155
+ task_counter: 'task-counter',
156
+ handler: @klass
157
+ }
158
+ @tasks = [
159
+ {id: 1},
160
+ {id: 2}
161
+ ]
162
+ @klass_ref = Kernel.const_get(@klass)
163
+ @worker = @klass_ref.new
164
+ redis = double('redis')
165
+ expect(redis).to receive(:incrby).with(@message[:task_counter], @tasks.count)
166
+ expect(redis).to receive(:incrby).with("#{@message[:task_counter]}.total", @tasks.count)
167
+ expect(Distribot).to receive(:redis).exactly(2).times{ redis }
168
+ expect(Distribot).to receive(:publish!).with(@message[:task_queue], hash_including({})).exactly(@tasks.count).times
169
+ end
170
+ it 'announces the new tasks on the task queue' do
171
+ context = OpenStruct.new(@message)
172
+ @worker.send(
173
+ :announce_tasks,
174
+ context,
175
+ @message,
176
+ @tasks
177
+ )
178
+ end
179
+ end
180
+ describe '#subscribe_to_task_queue' do
181
+ before do
182
+ @pool = double('pool')
183
+ expect(Concurrent::FixedThreadPool).to receive(:new) do
184
+ @pool
185
+ end
186
+ end
187
+ it 'subscribes to the task queue for this $flow.$phase.$handler so it can consume them, and stores the consumer for cancelation later' do
188
+ worker = @class_ref.new
189
+ expect(Distribot).to receive(:subscribe).with(@class_ref.task_queue, reenqueue_on_failure: true, solo: true) do |&block|
190
+ 'fake-consumer'
191
+ end
192
+ worker.subscribe_to_task_queue
193
+ end
194
+ context 'when it receives a task to work on' do
195
+ before do
196
+ expect(Distribot).to receive(:subscribe).with(@class_ref.task_queue, reenqueue_on_failure: true, solo: true) do |&block|
197
+ @callback = block
198
+ end
199
+ expect(@pool).to receive(:post) do |&block|
200
+ block.call
201
+ end
202
+ end
203
+ context 'and processing that task' do
204
+ context 'succeeds' do
205
+ it 'calls #process_single_task(contxt, task)' do
206
+ task = {some_task_thing: SecureRandom.uuid}
207
+ worker = @class_ref.new
208
+ expect(worker).to receive(:process_single_task).with(anything, task)
209
+ worker.subscribe_to_task_queue
210
+ @callback.call(task)
211
+ end
212
+ end
213
+ context 'raises an exception' do
214
+ it 'logs the error and re-raises the exception' do
215
+ task = {some_task_thing: SecureRandom.uuid}
216
+ worker = @class_ref.new
217
+ expect(worker).to receive(:process_single_task){ raise "Test Error" }
218
+ worker.subscribe_to_task_queue
219
+ expect(worker).to receive(:warn)
220
+ expect{@callback.call(task)}.to raise_error StandardError
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
226
+ describe '#process_single_task(context, task)' do
227
+ before do
228
+ @klass_ref = Kernel.const_get(@klass)
229
+ @worker = @klass_ref.new
230
+ end
231
+ context 'when the flow' do
232
+ before do
233
+ @flow = double('flow')
234
+ expect(Distribot::Flow).to receive(:find).with( 'xxx' ){ @flow }
235
+ @context = OpenStruct.new(
236
+ flow_id: 'xxx',
237
+ finished_queue: 'finished.queue',
238
+ phase: 'the-phase',
239
+ )
240
+ @task = { }
241
+ end
242
+ context 'is canceled' do
243
+ before do
244
+ expect(@flow).to receive(:canceled?){ true }
245
+ end
246
+ it 'raises an exception' do
247
+ expect{@worker.process_single_task(@context, @task)}.to raise_error Distribot::FlowCanceledError
248
+ end
249
+ end
250
+ context 'is paused' do
251
+ before do
252
+ expect(@flow).to receive(:paused?){ true }
253
+ expect(@flow).to receive(:canceled?){ false }
254
+ end
255
+ it 'raises an exception' do
256
+ expect{@worker.process_single_task(@context, @task)}.to raise_error Distribot::FlowPausedError
257
+ end
258
+ end
259
+ context 'is running' do
260
+ before do
261
+ expect(@flow).to receive(:paused?){ false }
262
+ expect(@flow).to receive(:canceled?){ false }
263
+ expect(Distribot).to receive(:publish!).with(@context.finished_queue, {
264
+ flow_id: 'xxx',
265
+ phase: 'the-phase',
266
+ handler: @klass
267
+ })
268
+ redis = double('redis')
269
+ expect(redis).to receive(:decr).with(
270
+ "distribot.flow.#{@context.flow_id}.#{@context.phase}.#{@klass}.finished"
271
+ )
272
+ expect(Distribot).to receive(:redis){ redis }
273
+ end
274
+ it 'calls the worker\'s processor callback, then announces that the task has been completed' do
275
+ @worker.process_single_task(@context, @task)
276
+ end
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "search",
3
+ "data": {
4
+ "flow_info": {
5
+ "foo": "bar"
6
+ }
7
+ },
8
+ "phases": [
9
+ {
10
+ "name": "pending",
11
+ "is_initial": true,
12
+ "transitions_to": "searching",
13
+ "on_error_transition_to": "error"
14
+ },
15
+ {
16
+ "name": "searching",
17
+ "transitions_to": "fetching-pages",
18
+ "on_error_transition_to": "error",
19
+ "handlers": [
20
+ {
21
+ "name": "GoogleSearcher",
22
+ "version": "~> 1.0"
23
+ }
24
+ ]
25
+ },
26
+ {
27
+ "name": "fetching-pages",
28
+ "transitions_to": "finished",
29
+ "on_error_transition_to": "error",
30
+ "handlers": [
31
+ "PageDownloader"
32
+ ]
33
+ },
34
+ {
35
+ "name": "error",
36
+ "is_final": true,
37
+ "handlers": [
38
+ "ErrorEmailer"
39
+ ]
40
+ },
41
+ {
42
+ "name": "finished",
43
+ "is_final": true,
44
+ "handlers": [
45
+ "JobFinisher"
46
+ ]
47
+ }
48
+ ]
49
+ }
@@ -0,0 +1,74 @@
1
+ # Did we get executed via 'rake'?
2
+ is_rake_exec = $0 =~ /\/rake/
3
+
4
+ unless is_rake_exec
5
+ ENV['RACK_ENV'] = 'test'
6
+ require 'codeclimate-test-reporter'
7
+ CodeClimate::TestReporter.start
8
+ require 'simplecov'
9
+ SimpleCov.start do
10
+ add_filter '.vendor/'
11
+ add_filter 'spec/'
12
+ end
13
+ SimpleCov.minimum_coverage 52
14
+ end
15
+
16
+ require 'rspec'
17
+ require 'bundler'
18
+ require 'shoulda-matchers'
19
+ require 'byebug'
20
+ require 'faker'
21
+ require 'webmock/rspec'
22
+
23
+ lib = File.expand_path('../../lib', __FILE__)
24
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
25
+
26
+ require 'distribot'
27
+
28
+ Bundler.load
29
+
30
+ def configure_rspec
31
+
32
+ RSpec.configure do |config|
33
+
34
+ config.expect_with :rspec do |expectations|
35
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
36
+ # Allow foo.should syntax:
37
+ expectations.syntax = [:should, :expect]
38
+ end
39
+
40
+ config.mock_with :rspec do |mocks|
41
+ mocks.syntax = [:should, :expect]
42
+ mocks.verify_partial_doubles = true
43
+ end
44
+
45
+ config.before :suite do
46
+ WebMock.enable!
47
+ WebMock.disable_net_connect!(
48
+ :allow_localhost => false,
49
+ :allow => 'codeclimate.com'
50
+ )
51
+ end
52
+
53
+ config.after :suite do
54
+ WebMock.disable!
55
+ end
56
+
57
+ config.run_all_when_everything_filtered = true
58
+
59
+ config.warnings = true
60
+
61
+ if config.files_to_run.one?
62
+ config.default_formatter = 'doc'
63
+ end
64
+
65
+ config.order = :random
66
+
67
+ Kernel.srand config.seed
68
+ end
69
+ end
70
+
71
+
72
+ unless is_rake_exec
73
+ configure_rspec()
74
+ end