distribot 0.1.1

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