distribot 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.rubocop.yml +3 -0
- data/.travis.yml +10 -0
- data/Dockerfile +9 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +153 -0
- data/LICENSE +201 -0
- data/README.md +107 -0
- data/Rakefile +16 -0
- data/bin/distribot.flow-created +6 -0
- data/bin/distribot.flow-finished +6 -0
- data/bin/distribot.handler-finished +6 -0
- data/bin/distribot.phase-finished +6 -0
- data/bin/distribot.phase-started +6 -0
- data/bin/distribot.task-finished +6 -0
- data/distribot.gemspec +35 -0
- data/docker-compose.yml +29 -0
- data/examples/controller +168 -0
- data/examples/distribot.eye +49 -0
- data/examples/status +38 -0
- data/examples/worker +135 -0
- data/lib/distribot/connector.rb +162 -0
- data/lib/distribot/flow.rb +200 -0
- data/lib/distribot/flow_created_handler.rb +12 -0
- data/lib/distribot/flow_finished_handler.rb +12 -0
- data/lib/distribot/handler.rb +40 -0
- data/lib/distribot/handler_finished_handler.rb +29 -0
- data/lib/distribot/phase.rb +46 -0
- data/lib/distribot/phase_finished_handler.rb +19 -0
- data/lib/distribot/phase_handler.rb +15 -0
- data/lib/distribot/phase_started_handler.rb +69 -0
- data/lib/distribot/task_finished_handler.rb +37 -0
- data/lib/distribot/worker.rb +148 -0
- data/lib/distribot.rb +108 -0
- data/provision/nodes.sh +80 -0
- data/provision/templates/fluentd.conf +27 -0
- data/spec/distribot/bunny_connector_spec.rb +196 -0
- data/spec/distribot/connection_sharer_spec.rb +34 -0
- data/spec/distribot/connector_spec.rb +63 -0
- data/spec/distribot/flow_created_handler_spec.rb +32 -0
- data/spec/distribot/flow_finished_handler_spec.rb +32 -0
- data/spec/distribot/flow_spec.rb +661 -0
- data/spec/distribot/handler_finished_handler_spec.rb +112 -0
- data/spec/distribot/handler_spec.rb +32 -0
- data/spec/distribot/module_spec.rb +163 -0
- data/spec/distribot/multi_subscription_spec.rb +37 -0
- data/spec/distribot/phase_finished_handler_spec.rb +61 -0
- data/spec/distribot/phase_started_handler_spec.rb +150 -0
- data/spec/distribot/subscription_spec.rb +40 -0
- data/spec/distribot/task_finished_handler_spec.rb +71 -0
- data/spec/distribot/worker_spec.rb +281 -0
- data/spec/fixtures/simple_flow.json +49 -0
- data/spec/spec_helper.rb +74 -0
- 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
|
+
}
|
data/spec/spec_helper.rb
ADDED
@@ -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
|