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