gush 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.rspec +1 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +128 -0
- data/Rakefile +1 -0
- data/bin/gush +12 -0
- data/gush.gemspec +32 -0
- data/lib/gush.rb +47 -0
- data/lib/gush/cli.rb +245 -0
- data/lib/gush/client.rb +146 -0
- data/lib/gush/configuration.rb +42 -0
- data/lib/gush/errors.rb +3 -0
- data/lib/gush/job.rb +161 -0
- data/lib/gush/logger_builder.rb +15 -0
- data/lib/gush/metadata.rb +24 -0
- data/lib/gush/null_logger.rb +6 -0
- data/lib/gush/version.rb +3 -0
- data/lib/gush/worker.rb +100 -0
- data/lib/gush/workflow.rb +154 -0
- data/spec/Gushfile.rb +0 -0
- data/spec/lib/gush/client_spec.rb +125 -0
- data/spec/lib/gush/configuration_spec.rb +27 -0
- data/spec/lib/gush/job_spec.rb +114 -0
- data/spec/lib/gush/logger_builder_spec.rb +25 -0
- data/spec/lib/gush/null_logger_spec.rb +15 -0
- data/spec/lib/gush/worker_spec.rb +96 -0
- data/spec/lib/gush/workflow_spec.rb +246 -0
- data/spec/lib/gush_spec.rb +39 -0
- data/spec/redis.conf +2 -0
- data/spec/spec_helper.rb +79 -0
- metadata +256 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Gush::NullLogger do
|
4
|
+
let(:logger) { Gush::NullLogger.new }
|
5
|
+
|
6
|
+
it 'responds to logger methods and ignores them' do
|
7
|
+
[:info, :debug, :error, :fatal].each do |method|
|
8
|
+
logger.send(method, "message")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'works when block with message is passed' do
|
13
|
+
logger.info("progname") { "message" }
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Gush::Worker do
|
4
|
+
let(:workflow_id) { '1234' }
|
5
|
+
let(:workflow) { TestWorkflow.new(workflow_id) }
|
6
|
+
let(:job) { workflow.find_job("Prepare") }
|
7
|
+
let(:config) { client.configuration.to_json }
|
8
|
+
|
9
|
+
before :each do
|
10
|
+
allow(client).to receive(:find_workflow).with(workflow_id).and_return(workflow)
|
11
|
+
allow(Gush::Client).to receive(:new).and_return(client)
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "#perform" do
|
15
|
+
context "when job fails" do
|
16
|
+
before :each do
|
17
|
+
expect(job).to receive(:work).and_raise(StandardError)
|
18
|
+
job.enqueue!
|
19
|
+
job.start!
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should mark it as failed" do
|
23
|
+
allow(client).to receive(:persist_job)
|
24
|
+
Gush::Worker.new.perform(workflow_id, "Prepare", config)
|
25
|
+
|
26
|
+
expect(client).to have_received(:persist_job).with(workflow_id, job).at_least(1).times do |_, job|
|
27
|
+
expect(job).to be_failed
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
it "reports that job failed" do
|
33
|
+
allow(client).to receive(:worker_report)
|
34
|
+
Gush::Worker.new.perform(workflow_id, "Prepare", config)
|
35
|
+
expect(client).to have_received(:worker_report).with(hash_including(status: :failed))
|
36
|
+
end
|
37
|
+
|
38
|
+
it "logs the exception" do
|
39
|
+
logger = TestLogger.new(1234, 'Prepare')
|
40
|
+
expect(logger).to receive(:<<).with(instance_of(String)).at_least(1).times
|
41
|
+
expect(workflow).to receive(:build_logger_for_job).and_return(logger)
|
42
|
+
|
43
|
+
Gush::Worker.new.perform(workflow_id, "Prepare", config)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context "when job completes successfully" do
|
48
|
+
it "should mark it as succedeed" do
|
49
|
+
allow(client).to receive(:persist_job)
|
50
|
+
|
51
|
+
Gush::Worker.new.perform(workflow_id, "Prepare", config)
|
52
|
+
|
53
|
+
expect(client).to have_received(:persist_job).at_least(1).times.with(workflow_id, job) do |_, job|
|
54
|
+
expect(job).to be_succeeded
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
it "reports that job succedeed" do
|
59
|
+
allow(client).to receive(:worker_report)
|
60
|
+
Gush::Worker.new.perform(workflow_id, "Prepare", config)
|
61
|
+
|
62
|
+
expect(client).to have_received(:worker_report).with(hash_including(status: :finished))
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
[:before_work, :work, :after_work].each do |method|
|
67
|
+
it "calls job.#{method} hook" do
|
68
|
+
expect(job).to receive(method)
|
69
|
+
Gush::Worker.new.perform(workflow_id, "Prepare", config)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
it "sets up a logger for the job" do
|
74
|
+
Gush::Worker.new.perform(workflow_id, "Prepare", config)
|
75
|
+
job.start!
|
76
|
+
expect(job.logger).to be_a TestLogger
|
77
|
+
end
|
78
|
+
|
79
|
+
it "sets a job id" do
|
80
|
+
job_id = 1234
|
81
|
+
worker = Gush::Worker.new
|
82
|
+
|
83
|
+
allow(worker).to receive(:jid).and_return(job_id)
|
84
|
+
|
85
|
+
worker.perform(workflow_id, "Prepare", config)
|
86
|
+
job.enqueue!
|
87
|
+
expect(job.jid).to eq job_id
|
88
|
+
end
|
89
|
+
|
90
|
+
it "reports when the job is started" do
|
91
|
+
allow(client).to receive(:worker_report)
|
92
|
+
Gush::Worker.new.perform(workflow_id, "Prepare", config)
|
93
|
+
expect(client).to have_received(:worker_report).with(hash_including(status: :started))
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,246 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Gush::Workflow do
|
4
|
+
subject { TestWorkflow.new("test-workflow") }
|
5
|
+
|
6
|
+
describe "#initialize" do
|
7
|
+
context "when configure option is true" do
|
8
|
+
it "runs #configure method " do
|
9
|
+
expect_any_instance_of(TestWorkflow).to receive(:configure)
|
10
|
+
TestWorkflow.new("name", configure: true)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "#stop!" do
|
16
|
+
it "marks workflow as stopped" do
|
17
|
+
expect{ subject.stop! }.to change{subject.stopped?}.from(false).to(true)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "#start!" do
|
22
|
+
it "removes stopped flag" do
|
23
|
+
subject.stopped = true
|
24
|
+
expect{ subject.start! }.to change{subject.stopped?}.from(true).to(false)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "#to_json" do
|
29
|
+
it "returns correct hash" do
|
30
|
+
|
31
|
+
klass = Class.new(Gush::Workflow) do
|
32
|
+
def configure
|
33
|
+
run FetchFirstJob
|
34
|
+
run PersistFirstJob, after: FetchFirstJob
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
result = JSON.parse(klass.new("workflow").to_json)
|
39
|
+
expected = {
|
40
|
+
"id"=>"workflow",
|
41
|
+
"name" => klass.to_s,
|
42
|
+
"klass" => klass.to_s,
|
43
|
+
"status" => "Pending",
|
44
|
+
"total" => 2,
|
45
|
+
"finished" => 0,
|
46
|
+
"started_at" => nil,
|
47
|
+
"finished_at" => nil,
|
48
|
+
"stopped" => false,
|
49
|
+
"logger_builder" => "Gush::LoggerBuilder",
|
50
|
+
"nodes" => [
|
51
|
+
{
|
52
|
+
"name"=>"FetchFirstJob", "klass"=>"FetchFirstJob", "finished"=>false, "enqueued"=>false, "failed"=>false,
|
53
|
+
"incoming"=>[], "outgoing"=>["PersistFirstJob"], "finished_at"=>nil, "started_at"=>nil, "failed_at"=>nil,
|
54
|
+
"running" => false
|
55
|
+
},
|
56
|
+
{
|
57
|
+
"name"=>"PersistFirstJob", "klass"=>"PersistFirstJob", "finished"=>false, "enqueued"=>false, "failed"=>false,
|
58
|
+
"incoming"=>["FetchFirstJob"], "outgoing"=>[], "finished_at"=>nil, "started_at"=>nil, "failed_at"=>nil,
|
59
|
+
"running" => false
|
60
|
+
}
|
61
|
+
]
|
62
|
+
}
|
63
|
+
expect(result).to eq(expected)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe "#find_job" do
|
68
|
+
it "finds job by its name" do
|
69
|
+
expect(TestWorkflow.new("test").find_job("PersistFirstJob")).to be_instance_of(PersistFirstJob)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "#run" do
|
74
|
+
context "when graph is empty" do
|
75
|
+
it "adds new job with the given class as a node" do
|
76
|
+
flow = Gush::Workflow.new("workflow")
|
77
|
+
flow.run(Gush::Job)
|
78
|
+
expect(flow.nodes.first).to be_instance_of(Gush::Job)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context "when last node is a job" do
|
83
|
+
it "attaches job as a child of the last inserted job" do
|
84
|
+
tree = Gush::Workflow.new("workflow")
|
85
|
+
klass1 = Class.new(Gush::Job)
|
86
|
+
klass2 = Class.new(Gush::Job)
|
87
|
+
tree.run(klass1)
|
88
|
+
tree.run(klass2, after: klass1)
|
89
|
+
tree.create_dependencies
|
90
|
+
expect(tree.nodes.first).to be_an_instance_of(klass1)
|
91
|
+
expect(tree.nodes.first.outgoing.first).to eq(klass2.to_s)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe "#logger_builder" do
|
97
|
+
it 'sets logger builder for workflow' do
|
98
|
+
tree = Gush::Workflow.new("workflow")
|
99
|
+
tree.logger_builder(TestLoggerBuilder)
|
100
|
+
expect(tree.instance_variable_get(:@logger_builder)).to eq(TestLoggerBuilder)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe "#build_logger_for_job" do
|
105
|
+
it 'builds a logger' do
|
106
|
+
job = double('job')
|
107
|
+
allow(job).to receive(:name) { 'a-job' }
|
108
|
+
|
109
|
+
tree = Gush::Workflow.new("workflow")
|
110
|
+
tree.logger_builder(TestLoggerBuilder)
|
111
|
+
|
112
|
+
logger = tree.build_logger_for_job(job, :jid)
|
113
|
+
expect(logger).to be_a(TestLogger)
|
114
|
+
expect(logger.jid).to eq(:jid)
|
115
|
+
expect(logger.name).to eq('a-job')
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
describe "#failed?" do
|
120
|
+
context "when one of the jobs failed" do
|
121
|
+
it "returns true" do
|
122
|
+
subject.find_job('Prepare').failed = true
|
123
|
+
expect(subject.failed?).to be_truthy
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
context "when no jobs failed" do
|
128
|
+
it "returns true" do
|
129
|
+
expect(subject.failed?).to be_falsy
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
describe "#running?" do
|
135
|
+
context "when no enqueued or running jobs" do
|
136
|
+
it "returns false" do
|
137
|
+
expect(subject.running?).to be_falsy
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
context "when some jobs are enqueued" do
|
142
|
+
it "returns true" do
|
143
|
+
subject.find_job('Prepare').enqueued = true
|
144
|
+
expect(subject.running?).to be_truthy
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
context "when some jobs are running" do
|
149
|
+
it "returns true" do
|
150
|
+
subject.find_job('Prepare').running = true
|
151
|
+
expect(subject.running?).to be_truthy
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
describe "#finished?" do
|
157
|
+
it "returns false if any jobs are unfinished" do
|
158
|
+
expect(subject.finished?).to be_falsy
|
159
|
+
end
|
160
|
+
|
161
|
+
it "returns true if all jobs are finished" do
|
162
|
+
subject.nodes.each {|n| n.finished = true }
|
163
|
+
expect(subject.finished?).to be_truthy
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
describe "#next_jobs" do
|
168
|
+
context "when one of the dependent jobs failed" do
|
169
|
+
it "returns only jobs with satisfied dependencies" do
|
170
|
+
subject.find_job('Prepare').finished = true
|
171
|
+
subject.find_job('FetchFirstJob').failed = true
|
172
|
+
expect(subject.next_jobs.map(&:name)).to match_array(["FetchSecondJob"])
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
it "returns next non-queued and unfinished jobs" do
|
177
|
+
expect(subject.next_jobs.map(&:name)).to match_array(["Prepare"])
|
178
|
+
end
|
179
|
+
|
180
|
+
it "returns all parallel non-queued and unfinished jobs" do
|
181
|
+
subject.find_job('Prepare').finished = true
|
182
|
+
expect(subject.next_jobs.map(&:name)).to match_array(["FetchFirstJob", "FetchSecondJob"])
|
183
|
+
end
|
184
|
+
|
185
|
+
it "returns empty array when there are enqueued but unfinished jobs" do
|
186
|
+
subject.find_job('Prepare').enqueued = true
|
187
|
+
expect(subject.next_jobs).to match_array([])
|
188
|
+
end
|
189
|
+
|
190
|
+
it "returns only unfinished and non-queued jobs from a parallel level" do
|
191
|
+
subject.find_job('Prepare').finished = true
|
192
|
+
subject.find_job('FetchFirstJob').finished = true
|
193
|
+
expect(subject.next_jobs.map(&:name)).to match_array(["PersistFirstJob", "FetchSecondJob"])
|
194
|
+
end
|
195
|
+
|
196
|
+
it "returns next level of unfished jobs after finished parallel level" do
|
197
|
+
subject.find_job('Prepare').finished = true
|
198
|
+
subject.find_job('PersistFirstJob').finished = true
|
199
|
+
subject.find_job('FetchFirstJob').finished = true
|
200
|
+
subject.find_job('FetchSecondJob').finished = true
|
201
|
+
expect(subject.next_jobs.map(&:name)).to match_array(["NormalizeJob"])
|
202
|
+
end
|
203
|
+
|
204
|
+
context "when mixing parallel tasks with synchronous" do
|
205
|
+
it "properly orders nested synchronous flows inside concurrent" do
|
206
|
+
flow = Gush::Workflow.new("workflow")
|
207
|
+
|
208
|
+
flow.run Prepare
|
209
|
+
flow.run NormalizeJob
|
210
|
+
|
211
|
+
flow.run FetchFirstJob, after: Prepare
|
212
|
+
flow.run PersistFirstJob, after: FetchFirstJob, before: NormalizeJob
|
213
|
+
flow.run FetchSecondJob, after: Prepare
|
214
|
+
flow.run PersistSecondJob, after: FetchSecondJob, before: NormalizeJob
|
215
|
+
|
216
|
+
flow.create_dependencies
|
217
|
+
expect(flow.next_jobs.map(&:name)).to match_array(["Prepare"])
|
218
|
+
flow.find_job("Prepare").finished = true
|
219
|
+
expect(flow.next_jobs.map(&:name)).to match_array(["FetchFirstJob", "FetchSecondJob"])
|
220
|
+
flow.find_job("FetchFirstJob").finished = true
|
221
|
+
expect(flow.next_jobs.map(&:name)).to match_array(["FetchSecondJob", "PersistFirstJob"])
|
222
|
+
flow.find_job("FetchSecondJob").finished = true
|
223
|
+
expect(flow.next_jobs.map(&:name)).to match_array(["PersistFirstJob", "PersistSecondJob"])
|
224
|
+
flow.find_job("PersistFirstJob").finished = true
|
225
|
+
expect(flow.next_jobs.map(&:name)).to match_array(["PersistSecondJob"])
|
226
|
+
flow.find_job("PersistSecondJob").finished = true
|
227
|
+
expect(flow.next_jobs.map(&:name)).to match_array(["NormalizeJob"])
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
it "fails when dependency resolution recurses too deep" do
|
232
|
+
flow = Gush::Workflow.new("workflow")
|
233
|
+
klass1 = Class.new(Gush::Job)
|
234
|
+
klass2 = Class.new(Gush::Job)
|
235
|
+
klass3 = Class.new(Gush::Job)
|
236
|
+
flow.run(klass1, after: klass3)
|
237
|
+
flow.run(klass2, after: klass1)
|
238
|
+
flow.run(klass3, after: klass2)
|
239
|
+
flow.create_dependencies
|
240
|
+
|
241
|
+
expect {
|
242
|
+
flow.next_jobs
|
243
|
+
}.to raise_error(DependencyLevelTooDeep)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Gush do
|
4
|
+
describe ".gushfile" do
|
5
|
+
let(:path) { Pathname("/tmp/Gushfile.rb") }
|
6
|
+
|
7
|
+
context "Gushfile.rb is missing from pwd" do
|
8
|
+
it "raises an exception" do
|
9
|
+
path.delete if path.exist?
|
10
|
+
Gush.configuration.gushfile = path
|
11
|
+
|
12
|
+
expect { Gush.gushfile }.to raise_error(Thor::Error)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
context "Gushfile.rb exists" do
|
17
|
+
it "returns Pathname to it" do
|
18
|
+
FileUtils.touch(path)
|
19
|
+
Gush.configuration.gushfile = path
|
20
|
+
expect(Gush.gushfile).to eq(path.realpath)
|
21
|
+
path.delete
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe ".root" do
|
27
|
+
it "returns root directory of Gush" do
|
28
|
+
expected = Pathname.new(__FILE__).parent.parent.parent
|
29
|
+
expect(Gush.root).to eq(expected)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe ".configure" do
|
34
|
+
it "runs block with config instance passed" do
|
35
|
+
expect { |b| Gush.configure(&b) }.to yield_with_args(Gush.configuration)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
data/spec/redis.conf
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'gush'
|
2
|
+
require 'pry'
|
3
|
+
require 'sidekiq/testing'
|
4
|
+
require 'bbq/spawn'
|
5
|
+
|
6
|
+
Sidekiq::Logging.logger = nil
|
7
|
+
|
8
|
+
class Prepare < Gush::Job; end
|
9
|
+
class FetchFirstJob < Gush::Job; end
|
10
|
+
class FetchSecondJob < Gush::Job; end
|
11
|
+
class PersistFirstJob < Gush::Job; end
|
12
|
+
class PersistSecondJob < Gush::Job; end
|
13
|
+
class NormalizeJob < Gush::Job; end
|
14
|
+
|
15
|
+
GUSHFILE = Pathname.new(__FILE__).parent.join("Gushfile.rb")
|
16
|
+
|
17
|
+
TestLogger = Struct.new(:jid, :name) do
|
18
|
+
def <<(msg)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class TestLoggerBuilder < Gush::LoggerBuilder
|
23
|
+
def build
|
24
|
+
TestLogger.new(jid, job.name)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class TestWorkflow < Gush::Workflow
|
29
|
+
def configure
|
30
|
+
logger_builder TestLoggerBuilder
|
31
|
+
|
32
|
+
run Prepare
|
33
|
+
|
34
|
+
run NormalizeJob
|
35
|
+
|
36
|
+
run FetchFirstJob, after: Prepare
|
37
|
+
run PersistFirstJob, after: FetchFirstJob, before: NormalizeJob
|
38
|
+
|
39
|
+
run FetchSecondJob, after: Prepare, before: NormalizeJob
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
module GushHelpers
|
44
|
+
REDIS_URL = "redis://localhost:33333/"
|
45
|
+
|
46
|
+
def redis
|
47
|
+
@redis ||= Redis.new(url: REDIS_URL)
|
48
|
+
end
|
49
|
+
|
50
|
+
def client
|
51
|
+
@client ||= Gush::Client.new(Gush::Configuration.new(gushfile: GUSHFILE, redis_url: REDIS_URL))
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
RSpec.configure do |config|
|
56
|
+
config.include GushHelpers
|
57
|
+
|
58
|
+
config.mock_with :rspec do |mocks|
|
59
|
+
mocks.verify_partial_doubles = true
|
60
|
+
end
|
61
|
+
|
62
|
+
orchestrator = Bbq::Spawn::Orchestrator.new
|
63
|
+
|
64
|
+
config.before(:suite) do
|
65
|
+
config_path = Pathname.pwd + "spec/redis.conf"
|
66
|
+
executor = Bbq::Spawn::Executor.new("redis-server", config_path.to_path)
|
67
|
+
orchestrator.coordinate(executor, host: '127.0.0.1', port: 33333)
|
68
|
+
orchestrator.start
|
69
|
+
end
|
70
|
+
|
71
|
+
config.after(:suite) do
|
72
|
+
orchestrator.stop
|
73
|
+
end
|
74
|
+
|
75
|
+
config.after(:each) do
|
76
|
+
Sidekiq::Worker.clear_all
|
77
|
+
redis.flushdb
|
78
|
+
end
|
79
|
+
end
|