gush 0.0.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 +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
|