burstflow 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,180 @@
1
+ require 'spec_helper'
2
+
3
+ describe Burst::Manager do
4
+ class JobHandler
5
+
6
+ attr_accessor :jobs
7
+
8
+ def initialize
9
+ @jobs = []
10
+ end
11
+
12
+ def add(json)
13
+ @jobs.push json
14
+ end
15
+
16
+ def find_job(klass)
17
+ @jobs.detect do |json|
18
+ json['klass'].to_s == klass.to_s
19
+ end
20
+ end
21
+
22
+ end
23
+
24
+ around :each do |ex|
25
+ begin
26
+ $job_handler = JobHandler.new
27
+ ex.run
28
+ ensure
29
+ $job_handler = nil
30
+ end
31
+ end
32
+
33
+ class TestCaseJob < Burst::Job
34
+
35
+ def perform
36
+ # puts "#{self.class.to_s}"
37
+ set_output(self.class.to_s)
38
+ $job_handler.add as_json.with_indifferent_access.merge(payloads: payloads)
39
+ end
40
+
41
+ end
42
+
43
+ class CaseJob1 < TestCaseJob; end
44
+ class CaseJob2 < TestCaseJob; end
45
+ class CaseJob3 < TestCaseJob; end
46
+ class CaseAsyncJob < TestCaseJob
47
+
48
+ def perform
49
+ suspend
50
+ $job_handler.add as_json.with_indifferent_access.merge(payloads: payloads)
51
+ end
52
+
53
+ end
54
+
55
+ class CaseW1 < Burst::Workflow
56
+
57
+ configure do |*_args|
58
+ id1 = run CaseJob1, id: 'job1', params: { p1: 1 }
59
+ run CaseJob2, id: 'job2', after: id1, params: { p2: 2 }
60
+ run CaseJob3, after: [CaseJob1, 'job2']
61
+ end
62
+
63
+ end
64
+
65
+
66
+ it 'case1 no async job state' do
67
+ w = CaseW1.build
68
+
69
+ perform_enqueued_jobs do
70
+ w.start!
71
+ end
72
+
73
+ w.reload
74
+
75
+ expect(w.started?).to eq true
76
+ expect(w.failed?).to eq false
77
+ expect(w.finished?).to eq true
78
+ expect(w.running?).to eq false
79
+ expect(w.status).to eq Burst::Workflow::FINISHED
80
+
81
+ expect($job_handler.jobs.count).to eq 3
82
+ expect($job_handler.find_job(CaseJob1)).to include(output: 'CaseJob1', params: { p1: 1 })
83
+
84
+ expect($job_handler.find_job(CaseJob2)).to include(output: 'CaseJob2', params: { p2: 2 })
85
+ expect($job_handler.find_job(CaseJob2)[:payloads]).to include(id: 'job1', class: 'CaseJob1', payload: 'CaseJob1')
86
+
87
+ expect($job_handler.find_job(CaseJob3)).to include(output: 'CaseJob3')
88
+ expect($job_handler.find_job(CaseJob3)[:payloads]).to include(id: 'job1', class: 'CaseJob1', payload: 'CaseJob1')
89
+ expect($job_handler.find_job(CaseJob3)[:payloads]).to include(id: 'job2', class: 'CaseJob2', payload: 'CaseJob2')
90
+ end
91
+
92
+ class CaseW2 < Burst::Workflow
93
+
94
+ configure do |*_args|
95
+ id1 = run CaseJob1, id: 'job1', params: { p1: 1 }
96
+ run CaseAsyncJob, id: 'job2', after: id1, params: { p2: 2 }
97
+ run CaseJob3, after: [CaseJob1, 'job2']
98
+ end
99
+
100
+ end
101
+
102
+ it 'case2 with async job state' do
103
+ w = CaseW2.build
104
+
105
+ perform_enqueued_jobs do
106
+ w.start!
107
+ end
108
+
109
+ w.reload
110
+
111
+ expect(w.started?).to eq true
112
+ expect(w.failed?).to eq false
113
+ expect(w.finished?).to eq false
114
+ expect(w.running?).to eq true
115
+ expect(w.suspended?).to eq true
116
+ expect(w.status).to eq Burst::Workflow::SUSPENDED
117
+
118
+ expect($job_handler.jobs.count).to eq 2
119
+ expect($job_handler.find_job(CaseJob1)).to include(output: 'CaseJob1', params: { p1: 1 })
120
+
121
+ expect($job_handler.find_job(CaseAsyncJob)).to include(output: Burst::Job::SUSPEND, params: { p2: 2 })
122
+ expect($job_handler.find_job(CaseAsyncJob)[:payloads]).to include(id: 'job1', class: 'CaseJob1', payload: 'CaseJob1')
123
+
124
+ w = CaseW2.find(w.id)
125
+
126
+ perform_enqueued_jobs do
127
+ w.resume!('job2', 'result')
128
+ end
129
+
130
+ w.reload
131
+
132
+ expect($job_handler.jobs.count).to eq 3
133
+ expect($job_handler.find_job(CaseJob1)).to include(output: 'CaseJob1', params: { p1: 1 })
134
+
135
+ # expect($job_handler.find_job(AsyncJob)).to include(output: 'result', params: {p2: 2})
136
+ expect($job_handler.find_job(CaseAsyncJob)[:payloads]).to include(id: 'job1', class: 'CaseJob1', payload: 'CaseJob1')
137
+
138
+ expect($job_handler.find_job(CaseJob3)).to include(output: 'CaseJob3')
139
+ expect($job_handler.find_job(CaseJob3)[:payloads]).to include(id: 'job1', class: 'CaseJob1', payload: 'CaseJob1')
140
+ expect($job_handler.find_job(CaseJob3)[:payloads]).to include(id: 'job2', class: 'CaseAsyncJob', payload: 'result')
141
+ end
142
+
143
+ describe 'dynamic jobs with payloads' do
144
+ class DynJob1 < TestCaseJob
145
+
146
+ def perform
147
+ configure do
148
+ run DynJob2, params: { a: 1 }
149
+ run DynJob2, params: { a: 2 }
150
+ run DynJob2, params: { a: 3 }
151
+ end
152
+ super
153
+ end
154
+
155
+ end
156
+
157
+ class DynJob2 < TestCaseJob; end
158
+ class DynJob3 < TestCaseJob; end
159
+
160
+ class DynFlow1 < Burst::Workflow
161
+
162
+ configure do |*_args|
163
+ run DynJob1
164
+ run DynJob3, after: DynJob1
165
+ end
166
+
167
+ end
168
+
169
+ it 'create intemediate jobs' do
170
+ w = DynFlow1.build
171
+
172
+ perform_enqueued_jobs do
173
+ w.start!
174
+ end
175
+
176
+ expect($job_handler.jobs.count).to eq 5
177
+ expect($job_handler.jobs.last['payloads'].count).to eq 4
178
+ end
179
+ end
180
+ end
data/spec/job_spec.rb ADDED
@@ -0,0 +1,80 @@
1
+ require 'spec_helper'
2
+
3
+ describe Burst::Job do
4
+ let(:w) { Burst::Workflow.new }
5
+
6
+ context 'initializing' do
7
+ class TestJob1 < Burst::Job
8
+ end
9
+
10
+ subject{ TestJob1.new(w, {}.with_indifferent_access) }
11
+
12
+ it 'empty' do
13
+ expect(subject.workflow_id).to eq w.id
14
+ expect(subject.klass).to eq TestJob1.to_s
15
+
16
+ expect(subject.incoming).to eq []
17
+ expect(subject.outgoing).to eq []
18
+
19
+ expect(subject.initial?).to eq true
20
+
21
+ expect(subject.enqueued?).to eq false
22
+ expect(subject.started?).to eq false
23
+ expect(subject.finished?).to eq false
24
+ expect(subject.failed?).to eq false
25
+ expect(subject.suspended?).to eq false
26
+ expect(subject.resumed?).to eq false
27
+
28
+ expect(subject.ready_to_start?).to eq true
29
+ end
30
+
31
+ it '#enqueue!' do
32
+ subject.enqueue!
33
+
34
+ expect(subject.enqueued?).to eq true
35
+ expect(subject.ready_to_start?).to eq false
36
+ end
37
+
38
+ it '#start!' do
39
+ subject.start!
40
+
41
+ expect(subject.started?).to eq true
42
+ expect(subject.ready_to_start?).to eq false
43
+ end
44
+
45
+ it '#finish!' do
46
+ subject.finish!
47
+
48
+ expect(subject.finished?).to eq true
49
+ expect(subject.succeeded?).to eq true
50
+ expect(subject.failed?).to eq false
51
+ expect(subject.ready_to_start?).to eq false
52
+ end
53
+
54
+ it '#fail!' do
55
+ subject.fail!
56
+
57
+ expect(subject.finished?).to eq true
58
+ expect(subject.succeeded?).to eq false
59
+ expect(subject.failed?).to eq true
60
+ expect(subject.ready_to_start?).to eq false
61
+ end
62
+
63
+ it '#suspend!' do
64
+ subject.suspend!
65
+
66
+ expect(subject.finished?).to eq false
67
+ expect(subject.suspended?).to eq true
68
+ expect(subject.ready_to_start?).to eq true
69
+ end
70
+
71
+ it '#resume!' do
72
+ subject.suspend!
73
+ subject.resume!
74
+
75
+ expect(subject.finished?).to eq false
76
+ expect(subject.resumed?).to eq true
77
+ expect(subject.ready_to_start?).to eq true
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,47 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ require 'bundler/setup'
4
+ Bundler.require(:default)
5
+
6
+ require 'burst'
7
+
8
+
9
+ ActiveJob::Base.queue_adapter = :test
10
+ ActiveJob::Base.logger = nil
11
+
12
+ $root = File.join(File.dirname(__dir__), 'spec')
13
+ Dir[File.join($root, 'support', '**', '*.rb')].each {|f| require f }
14
+
15
+
16
+
17
+
18
+ RSpec::Matchers.define :have_jobs do |flow, jobs|
19
+ match do |_actual|
20
+ expected = jobs.map do |job|
21
+ hash_including(args: include(flow, job))
22
+ end
23
+ expect(ActiveJob::Base.queue_adapter.enqueued_jobs).to match_array(expected)
24
+ end
25
+
26
+ failure_message do |_actual|
27
+ "expected queue to have #{jobs}, but instead has: #{ActiveJob::Base.queue_adapter.enqueued_jobs.map{|j| j[:args][1] }}"
28
+ end
29
+ end
30
+
31
+ RSpec.configure do |config|
32
+ config.include ActiveJob::TestHelper
33
+
34
+ config.mock_with :rspec do |mocks|
35
+ mocks.verify_partial_doubles = true
36
+ end
37
+
38
+ config.before(:each) do
39
+ clear_enqueued_jobs
40
+ clear_performed_jobs
41
+ end
42
+
43
+ config.after(:each) do
44
+ clear_enqueued_jobs
45
+ clear_performed_jobs
46
+ end
47
+ end
@@ -0,0 +1,16 @@
1
+ require 'database_cleaner'
2
+
3
+ RSpec.configure do |config|
4
+ config.before(:all) do
5
+ DatabaseCleaner.clean_with(:truncation)
6
+ end
7
+
8
+ config.before(:each) do
9
+ DatabaseCleaner.strategy = :transaction
10
+ DatabaseCleaner.start
11
+ end
12
+
13
+ config.after(:each) do
14
+ DatabaseCleaner.clean
15
+ end
16
+ end
@@ -0,0 +1,12 @@
1
+ require 'otr-activerecord'
2
+ require 'yaml'
3
+ require 'erb'
4
+
5
+ RSpec.configure do |config|
6
+ # ActiveSupport::Dependencies.autoload_paths += [ 'lib' ].map {|f| File.join($root, '..', f)}
7
+
8
+ config.before(:suite) do
9
+ OTR::ActiveRecord.configure_from_file! 'config/database.yml'
10
+ ActiveRecord::Base.logger = nil
11
+ end
12
+ end
@@ -0,0 +1,185 @@
1
+ require 'spec_helper'
2
+
3
+ describe Burst::Workflow do
4
+ class TestJob < Burst::Job
5
+
6
+ def perform
7
+ # puts "#{self.class} perform"
8
+ end
9
+
10
+ end
11
+
12
+ class WfJob1 < TestJob
13
+ end
14
+
15
+ class WfJob2 < TestJob
16
+ end
17
+
18
+ class WfJob3 < TestJob
19
+ end
20
+
21
+
22
+ class W1 < Burst::Workflow
23
+
24
+ configure do |*_args|
25
+ id1 = run WfJob1, id: 'job1'
26
+ run WfJob2, id: 'job2', after: id1
27
+ run WfJob3, after: [WfJob1, 'job2']
28
+ end
29
+
30
+ end
31
+
32
+ def expect_jobs(*jobs)
33
+ j1, j2, j3 = *jobs
34
+
35
+ expect(j1.finished?).to eq false
36
+ expect(j2.finished?).to eq false
37
+
38
+ expect(j1.klass).to eq 'WfJob1'
39
+ expect(j2.klass).to eq 'WfJob2'
40
+ expect(j3.klass).to eq 'WfJob3'
41
+
42
+ expect(j1.outgoing).to include(j2.id, j3.id)
43
+ expect(j2.outgoing).to include(j3.id)
44
+ expect(j3.outgoing).to include
45
+
46
+ expect(j1.incoming).to include
47
+ expect(j2.incoming).to include(j1.id)
48
+ expect(j3.incoming).to include(j1.id, j2.id)
49
+
50
+ expect(j1.initial?).to eq true
51
+ expect(j2.initial?).to eq false
52
+ expect(j3.initial?).to eq false
53
+ end
54
+
55
+ context 'model' do
56
+ it 'default store' do
57
+ w = Burst::Workflow.new
58
+
59
+ expect(w.attributes).to include(:id, jobs: {}, klass: Burst::Workflow.to_s)
60
+
61
+ expect(w.started?).to eq false
62
+ expect(w.failed?).to eq false
63
+ expect(w.finished?).to eq true
64
+ expect(w.running?).to eq false
65
+ expect(w.status).to eq Burst::Workflow::FINISHED
66
+ end
67
+
68
+ it 'store persistance' do
69
+ w = Burst::Workflow.new
70
+ w.save!
71
+
72
+ w2 = Burst::Workflow.find(w.id)
73
+ expect(w2.attributes).to include(w.attributes)
74
+ end
75
+
76
+ it 'builded store' do
77
+ w = W1.build
78
+
79
+ jobs = w.jobs
80
+ expect_jobs(*jobs.values.map{|json| Burst::Job.new(w, json) })
81
+
82
+ expect(w.attributes).to include(:id, jobs: jobs, klass: W1.to_s)
83
+ end
84
+
85
+ it 'builded persistance' do
86
+ w = W1.build
87
+ w.save!
88
+
89
+ jobs = w.jobs
90
+ expect_jobs(*jobs.values.map{|json| Burst::Job.new(w, json) })
91
+
92
+ w2 = Burst::Workflow.find(w.id)
93
+ expect(w2.attributes).to include(w.attributes)
94
+ end
95
+ end
96
+
97
+ def expect_wf(w)
98
+ j1 = w.find_job('job1')
99
+ j2 = w.find_job('job2')
100
+ j3 = w.find_job(WfJob3)
101
+
102
+ expect(j1.finished?).to eq false
103
+ expect(j2.finished?).to eq false
104
+
105
+ expect(j1.klass).to eq 'WfJob1'
106
+ expect(j2.klass).to eq 'WfJob2'
107
+ expect(j3.klass).to eq 'WfJob3'
108
+
109
+ expect(j1.outgoing).to include(j2.id, j3.id)
110
+ expect(j2.outgoing).to include(j3.id)
111
+ expect(j3.outgoing).to include
112
+
113
+ expect(j1.incoming).to include
114
+ expect(j2.incoming).to include(j1.id)
115
+ expect(j3.incoming).to include(j1.id, j2.id)
116
+
117
+ expect(j1.initial?).to eq true
118
+ expect(j2.initial?).to eq false
119
+ expect(j3.initial?).to eq false
120
+
121
+ expect(w.initial_jobs).to include(j1)
122
+ end
123
+
124
+ it 'check builder' do
125
+ w = W1.build
126
+ expect_wf(w)
127
+
128
+ expect(w.status)
129
+ end
130
+
131
+ it 'check persistance' do
132
+ w = W1.build
133
+ w.save!
134
+
135
+ w2 = W1.find(w.id)
136
+ expect_wf(w2)
137
+ end
138
+
139
+ it 'start without perform' do
140
+ w = W1.build
141
+ w.save!
142
+
143
+ expect(w.started?).to eq false
144
+ expect(w.failed?).to eq false
145
+ expect(w.finished?).to eq false
146
+ expect(w.running?).to eq false
147
+ expect(w.status).to eq Burst::Workflow::INITIAL
148
+
149
+ w.start!
150
+
151
+ expect(Burst::Worker).to have_jobs(w.id, ['job1'])
152
+ expect(Burst::Worker).not_to have_jobs(w.id, ['job2'])
153
+
154
+ w = W1.find(w.id)
155
+
156
+ expect(w.started?).to eq false
157
+ expect(w.failed?).to eq false
158
+ expect(w.finished?).to eq false
159
+ expect(w.running?).to eq false
160
+ expect(w.status).to eq Burst::Workflow::INITIAL
161
+ end
162
+
163
+ it 'start with perform' do
164
+ w = W1.build
165
+ w.save!
166
+
167
+ expect(w.started?).to eq false
168
+ expect(w.failed?).to eq false
169
+ expect(w.finished?).to eq false
170
+ expect(w.running?).to eq false
171
+ expect(w.status).to eq Burst::Workflow::INITIAL
172
+
173
+ perform_enqueued_jobs do
174
+ w.start!
175
+ end
176
+
177
+ w = W1.find(w.id)
178
+
179
+ expect(w.started?).to eq true
180
+ expect(w.failed?).to eq false
181
+ expect(w.finished?).to eq true
182
+ expect(w.running?).to eq false
183
+ expect(w.status).to eq Burst::Workflow::FINISHED
184
+ end
185
+ end
metadata ADDED
@@ -0,0 +1,149 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: burstflow
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Samoilenko Yuri
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-01-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activejob
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activerecord
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: It has dependency, result pipelining and suspend/resume ability
84
+ email:
85
+ - kinnalru@gmail.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".rspec"
92
+ - ".rubocop.yml"
93
+ - ".travis.yml"
94
+ - Gemfile
95
+ - README.md
96
+ - Rakefile
97
+ - burst.gemspec
98
+ - config/database.yml
99
+ - db/migrate/20180101000001_create_workflow.rb
100
+ - db/schema.rb
101
+ - db/seeds.rb
102
+ - lib/burst.rb
103
+ - lib/burst/builder.rb
104
+ - lib/burst/configuration.rb
105
+ - lib/burst/job.rb
106
+ - lib/burst/manager.rb
107
+ - lib/burst/model.rb
108
+ - lib/burst/worker.rb
109
+ - lib/burst/workflow.rb
110
+ - lib/burst/workflow_helper.rb
111
+ - spec/burst_spec.rb
112
+ - spec/cases_spec.rb
113
+ - spec/job_spec.rb
114
+ - spec/spec_helper.rb
115
+ - spec/support/database_clean.rb
116
+ - spec/support/runner.rb
117
+ - spec/workflow_spec.rb
118
+ homepage: https://github.com/RnD-Soft/burst
119
+ licenses:
120
+ - MIT
121
+ metadata: {}
122
+ post_install_message:
123
+ rdoc_options: []
124
+ require_paths:
125
+ - lib
126
+ required_ruby_version: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ required_rubygems_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ version: '0'
136
+ requirements: []
137
+ rubyforge_project:
138
+ rubygems_version: 2.6.14
139
+ signing_key:
140
+ specification_version: 4
141
+ summary: Burst is a parallel workflow runner using ActiveRecord and ActiveJob
142
+ test_files:
143
+ - spec/burst_spec.rb
144
+ - spec/cases_spec.rb
145
+ - spec/job_spec.rb
146
+ - spec/spec_helper.rb
147
+ - spec/support/database_clean.rb
148
+ - spec/support/runner.rb
149
+ - spec/workflow_spec.rb