burstflow 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rspec +3 -0
- data/.rubocop.yml +98 -0
- data/.travis.yml +31 -0
- data/Gemfile +16 -0
- data/README.md +293 -0
- data/Rakefile +4 -0
- data/burst.gemspec +23 -0
- data/config/database.yml +15 -0
- data/db/migrate/20180101000001_create_workflow.rb +13 -0
- data/db/schema.rb +23 -0
- data/db/seeds.rb +1 -0
- data/lib/burst/builder.rb +48 -0
- data/lib/burst/configuration.rb +27 -0
- data/lib/burst/job.rb +187 -0
- data/lib/burst/manager.rb +79 -0
- data/lib/burst/model.rb +49 -0
- data/lib/burst/worker.rb +42 -0
- data/lib/burst/workflow.rb +148 -0
- data/lib/burst/workflow_helper.rb +86 -0
- data/lib/burst.rb +37 -0
- data/spec/burst_spec.rb +4 -0
- data/spec/cases_spec.rb +180 -0
- data/spec/job_spec.rb +80 -0
- data/spec/spec_helper.rb +47 -0
- data/spec/support/database_clean.rb +16 -0
- data/spec/support/runner.rb +12 -0
- data/spec/workflow_spec.rb +185 -0
- metadata +149 -0
data/lib/burst/job.rb
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
class Burst::Job
|
2
|
+
|
3
|
+
include Burst::Model
|
4
|
+
|
5
|
+
define_stored_attributes :id, :workflow_id, :klass, :params, :incoming, :outgoing, :payloads, :output
|
6
|
+
define_stored_attributes :enqueued_at, :started_at, :finished_at, :failed_at, :suspended_at, :resumed_at
|
7
|
+
|
8
|
+
SUSPEND = 'suspend'.freeze
|
9
|
+
|
10
|
+
class Error < ::RuntimeError; end
|
11
|
+
|
12
|
+
def initialize(workflow, hash_store = {})
|
13
|
+
@workflow = workflow
|
14
|
+
assign_default_values(hash_store)
|
15
|
+
end
|
16
|
+
|
17
|
+
def assign_default_values(hash_store)
|
18
|
+
set_model(hash_store.deep_dup)
|
19
|
+
|
20
|
+
self.id ||= SecureRandom.uuid
|
21
|
+
self.workflow_id ||= @workflow.id
|
22
|
+
self.klass ||= self.class.to_s
|
23
|
+
self.incoming ||= []
|
24
|
+
self.outgoing ||= []
|
25
|
+
end
|
26
|
+
|
27
|
+
def reload
|
28
|
+
assign_default_values(@workflow.get_job_hash(self.id))
|
29
|
+
end
|
30
|
+
|
31
|
+
def save!
|
32
|
+
@workflow.with_lock do
|
33
|
+
@workflow.set_job(self)
|
34
|
+
@workflow.save!
|
35
|
+
yield if block_given?
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.from_hash(workflow, hash_store)
|
40
|
+
hash_store[:klass].constantize.new(workflow, hash_store)
|
41
|
+
end
|
42
|
+
|
43
|
+
# execute this code by ActiveJob. You may return Burst::Job::SUSPEND to suspend job, or call suspend method
|
44
|
+
def perform; end
|
45
|
+
|
46
|
+
# execute this code when resumes after suspending
|
47
|
+
def resume(data)
|
48
|
+
set_output(data)
|
49
|
+
end
|
50
|
+
|
51
|
+
# store data to be available for next jobs
|
52
|
+
def set_output(data)
|
53
|
+
self.output = data
|
54
|
+
end
|
55
|
+
|
56
|
+
# mark execution as suspended
|
57
|
+
def suspend
|
58
|
+
set_output(SUSPEND)
|
59
|
+
end
|
60
|
+
|
61
|
+
def configure
|
62
|
+
@workflow.with_lock do
|
63
|
+
yield
|
64
|
+
@workflow.resolve_dependencies
|
65
|
+
@workflow.save!
|
66
|
+
@workflow.all_jobs.to_a.each(&:save!)
|
67
|
+
reload
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def run(klass, opts = {})
|
72
|
+
opts[:after] = [*opts[:after], self.id].uniq
|
73
|
+
opts[:before] = [*opts[:before], *self.outgoing].uniq
|
74
|
+
@workflow.run(klass, opts)
|
75
|
+
end
|
76
|
+
|
77
|
+
def attributes
|
78
|
+
{
|
79
|
+
workflow_id: self.workflow_id,
|
80
|
+
id: self.id,
|
81
|
+
klass: self.klass,
|
82
|
+
params: params,
|
83
|
+
incoming: self.incoming,
|
84
|
+
outgoing: self.outgoing,
|
85
|
+
output: output,
|
86
|
+
started_at: started_at,
|
87
|
+
enqueued_at: enqueued_at,
|
88
|
+
finished_at: finished_at,
|
89
|
+
failed_at: failed_at,
|
90
|
+
suspended_at: suspended_at,
|
91
|
+
resumed_at: resumed_at
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
# mark job as enqueued when it is scheduled to queue
|
96
|
+
def enqueue!
|
97
|
+
raise Error.new('Already enqueued') if enqueued?
|
98
|
+
self.enqueued_at = current_timestamp
|
99
|
+
self.started_at = nil
|
100
|
+
self.finished_at = nil
|
101
|
+
self.failed_at = nil
|
102
|
+
self.suspended_at = nil
|
103
|
+
self.resumed_at = nil
|
104
|
+
end
|
105
|
+
|
106
|
+
# mark job as started when it is start performing
|
107
|
+
def start!
|
108
|
+
raise Error.new('Already started') if started?
|
109
|
+
self.started_at = current_timestamp
|
110
|
+
end
|
111
|
+
|
112
|
+
# mark job as finished when it is finish performing
|
113
|
+
def finish!
|
114
|
+
raise Error.new('Already finished') if finished?
|
115
|
+
self.finished_at = current_timestamp
|
116
|
+
end
|
117
|
+
|
118
|
+
# mark job as failed when it is failed
|
119
|
+
def fail!
|
120
|
+
raise Error.new('Already failed') if failed?
|
121
|
+
self.finished_at = self.failed_at = current_timestamp
|
122
|
+
end
|
123
|
+
|
124
|
+
# mark job as suspended
|
125
|
+
def suspend!
|
126
|
+
self.suspended_at = current_timestamp
|
127
|
+
end
|
128
|
+
|
129
|
+
# mark job as resumed
|
130
|
+
def resume!
|
131
|
+
raise Error.new('Not suspended ') unless suspended?
|
132
|
+
raise Error.new('Already resumed ') if resumed?
|
133
|
+
self.resumed_at = current_timestamp
|
134
|
+
end
|
135
|
+
|
136
|
+
def enqueued?
|
137
|
+
!enqueued_at.nil?
|
138
|
+
end
|
139
|
+
|
140
|
+
def started?
|
141
|
+
!started_at.nil?
|
142
|
+
end
|
143
|
+
|
144
|
+
def finished?
|
145
|
+
!finished_at.nil?
|
146
|
+
end
|
147
|
+
|
148
|
+
def running?
|
149
|
+
started? && !finished? && !suspended?
|
150
|
+
end
|
151
|
+
|
152
|
+
def failed?
|
153
|
+
!failed_at.nil?
|
154
|
+
end
|
155
|
+
|
156
|
+
def suspended?
|
157
|
+
!suspended_at.nil? && !resumed?
|
158
|
+
end
|
159
|
+
|
160
|
+
def resumed?
|
161
|
+
!resumed_at.nil?
|
162
|
+
end
|
163
|
+
|
164
|
+
def succeeded?
|
165
|
+
finished? && !failed?
|
166
|
+
end
|
167
|
+
|
168
|
+
def ready_to_start?
|
169
|
+
!running? && !enqueued? && !finished? && !failed? && parents_succeeded?
|
170
|
+
end
|
171
|
+
|
172
|
+
def initial?
|
173
|
+
incoming.empty?
|
174
|
+
end
|
175
|
+
|
176
|
+
def current_timestamp
|
177
|
+
Time.now.to_i
|
178
|
+
end
|
179
|
+
|
180
|
+
def parents_succeeded?
|
181
|
+
incoming.all? do |id|
|
182
|
+
@workflow.get_job(id).succeeded?
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
|
187
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
class Burst::Manager
|
2
|
+
|
3
|
+
attr_accessor :workflow
|
4
|
+
|
5
|
+
def initialize(workflow)
|
6
|
+
@workflow = workflow
|
7
|
+
end
|
8
|
+
|
9
|
+
def start
|
10
|
+
workflow.with_lock do
|
11
|
+
raise 'Already started' unless workflow.initial?
|
12
|
+
|
13
|
+
workflow.initial_jobs.each do |job|
|
14
|
+
enqueue_job!(job)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def enqueue_job!(job)
|
20
|
+
job.enqueue!
|
21
|
+
job.save! do
|
22
|
+
Burst::Worker.perform_later(workflow.id, job.id)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def resume!(job, data)
|
27
|
+
job.resume!
|
28
|
+
job.save! do
|
29
|
+
Burst::Worker.perform_later(workflow.id, job.id, data)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def start_job!(job)
|
34
|
+
job.start!
|
35
|
+
job.save!
|
36
|
+
|
37
|
+
job.perform
|
38
|
+
end
|
39
|
+
|
40
|
+
def resume_job!(job, data)
|
41
|
+
job.resume(data)
|
42
|
+
end
|
43
|
+
|
44
|
+
def suspend_job!(job)
|
45
|
+
job.suspend!
|
46
|
+
job.save!
|
47
|
+
end
|
48
|
+
|
49
|
+
def finish_job!(job)
|
50
|
+
job.finish!
|
51
|
+
job.save!
|
52
|
+
|
53
|
+
workflow.with_lock do
|
54
|
+
enqueue_outgoing_jobs(job)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def job_performed!(job, result)
|
59
|
+
if result == Burst::Job::SUSPEND || job.output == Burst::Job::SUSPEND
|
60
|
+
suspend_job!(job)
|
61
|
+
else
|
62
|
+
finish_job!(job)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def fail_job!(job)
|
67
|
+
job.fail!
|
68
|
+
job.save!
|
69
|
+
end
|
70
|
+
|
71
|
+
def enqueue_outgoing_jobs(job)
|
72
|
+
job.outgoing.each do |job_id|
|
73
|
+
out = workflow.get_job(job_id)
|
74
|
+
|
75
|
+
enqueue_job!(out) if out.ready_to_start?
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
data/lib/burst/model.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
module Burst::Model
|
2
|
+
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do |klass|
|
6
|
+
klass.include ActiveModel::Model
|
7
|
+
klass.include ActiveModel::Dirty
|
8
|
+
klass.include ActiveModel::Serialization
|
9
|
+
klass.extend ActiveModel::Callbacks
|
10
|
+
|
11
|
+
attr_accessor :model
|
12
|
+
|
13
|
+
def set_model(model)
|
14
|
+
@model = model
|
15
|
+
end
|
16
|
+
|
17
|
+
def attributes=(hash)
|
18
|
+
hash.each do |key, value|
|
19
|
+
send("#{key}=", value)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def ==(other)
|
24
|
+
instance_variable_get('@model') == other.instance_variable_get('@model')
|
25
|
+
end
|
26
|
+
|
27
|
+
def as_json(*_args)
|
28
|
+
serializable_hash
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class_methods do
|
33
|
+
def define_stored_attributes(*keys)
|
34
|
+
keys.each do |key|
|
35
|
+
define_attribute_methods key.to_sym
|
36
|
+
|
37
|
+
define_method key.to_sym do
|
38
|
+
return @model[key.to_s]
|
39
|
+
end
|
40
|
+
|
41
|
+
define_method "#{key}=".to_sym do |v|
|
42
|
+
send("#{key}_will_change!") if v != send(key)
|
43
|
+
return @model[key.to_s] = v
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
data/lib/burst/worker.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
class Burst::Worker < ::ActiveJob::Base
|
2
|
+
|
3
|
+
def perform(workflow_id, job_id, resume_data = nil)
|
4
|
+
setup(workflow_id, job_id)
|
5
|
+
|
6
|
+
job.payloads = incoming_payloads
|
7
|
+
|
8
|
+
result = if resume_data.nil?
|
9
|
+
@manager.start_job!(job)
|
10
|
+
else
|
11
|
+
@manager.resume_job!(job, resume_data)
|
12
|
+
end
|
13
|
+
|
14
|
+
@manager.job_performed!(job, result)
|
15
|
+
rescue StandardError => e
|
16
|
+
@manager.fail_job!(job)
|
17
|
+
raise e
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :workflow, :job
|
24
|
+
|
25
|
+
def setup(workflow_id, job_id)
|
26
|
+
@workflow = Burst::Workflow.find(workflow_id)
|
27
|
+
@job = @workflow.get_job(job_id)
|
28
|
+
@manager = @workflow.manager
|
29
|
+
end
|
30
|
+
|
31
|
+
def incoming_payloads
|
32
|
+
job.incoming.map do |job_id|
|
33
|
+
incoming = workflow.get_job(job_id)
|
34
|
+
{
|
35
|
+
id: incoming.id,
|
36
|
+
class: incoming.klass.to_s,
|
37
|
+
payload: incoming.output
|
38
|
+
}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
class Burst::Workflow < ActiveRecord::Base
|
2
|
+
|
3
|
+
self.table_name_prefix = 'burst_'
|
4
|
+
|
5
|
+
INITIAL = 'initial'.freeze
|
6
|
+
RUNNING = 'running'.freeze
|
7
|
+
FINISHED = 'finished'.freeze
|
8
|
+
FAILED = 'failed'.freeze
|
9
|
+
SUSPENDED = 'suspended'.freeze
|
10
|
+
|
11
|
+
include Burst::WorkflowHelper
|
12
|
+
include Burst::Builder
|
13
|
+
|
14
|
+
attr_accessor :manager, :job_cache
|
15
|
+
define_flow_attributes :jobs, :klass
|
16
|
+
|
17
|
+
after_initialize do
|
18
|
+
initialize_builder
|
19
|
+
|
20
|
+
@job_cache = {}
|
21
|
+
|
22
|
+
self.id ||= SecureRandom.uuid
|
23
|
+
self.jobs ||= {}.with_indifferent_access
|
24
|
+
self.klass ||= self.class.to_s
|
25
|
+
|
26
|
+
@manager = Burst::Manager.new(self)
|
27
|
+
end
|
28
|
+
|
29
|
+
def attributes
|
30
|
+
{
|
31
|
+
id: self.id,
|
32
|
+
jobs: self.jobs,
|
33
|
+
klass: self.klass,
|
34
|
+
status: status
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.build(*args)
|
39
|
+
wf = new
|
40
|
+
wf.configure(*args)
|
41
|
+
wf.resolve_dependencies
|
42
|
+
wf
|
43
|
+
end
|
44
|
+
|
45
|
+
def reload(options = nil)
|
46
|
+
self.job_cache = {}
|
47
|
+
super
|
48
|
+
end
|
49
|
+
|
50
|
+
def start!
|
51
|
+
save!
|
52
|
+
manager.start
|
53
|
+
end
|
54
|
+
|
55
|
+
def resume!(job_id, data)
|
56
|
+
manager.resume!(get_job(job_id), data)
|
57
|
+
end
|
58
|
+
|
59
|
+
def status
|
60
|
+
if failed?
|
61
|
+
FAILED
|
62
|
+
elsif suspended?
|
63
|
+
SUSPENDED
|
64
|
+
elsif running?
|
65
|
+
RUNNING
|
66
|
+
elsif finished?
|
67
|
+
FINISHED
|
68
|
+
else
|
69
|
+
INITIAL
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def initial?
|
74
|
+
status == INITIAL
|
75
|
+
end
|
76
|
+
|
77
|
+
def finished?
|
78
|
+
all_jobs.all?(&:finished?)
|
79
|
+
end
|
80
|
+
|
81
|
+
def started?
|
82
|
+
!!started_at
|
83
|
+
end
|
84
|
+
|
85
|
+
def running?
|
86
|
+
started? && !finished?
|
87
|
+
end
|
88
|
+
|
89
|
+
def failed?
|
90
|
+
all_jobs.any?(&:failed?)
|
91
|
+
end
|
92
|
+
|
93
|
+
def suspended?
|
94
|
+
!failed? && all_jobs.any?(&:suspended?)
|
95
|
+
end
|
96
|
+
|
97
|
+
def all_jobs
|
98
|
+
Enumerator.new do |y|
|
99
|
+
jobs.keys.each do |id|
|
100
|
+
y << get_job(id)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def get_job(id)
|
106
|
+
if job = @job_cache[id]
|
107
|
+
job
|
108
|
+
else
|
109
|
+
job = Burst::Job.from_hash(self, jobs[id].deep_dup)
|
110
|
+
@job_cache[job.id] = job
|
111
|
+
job
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def set_job(job)
|
116
|
+
jobs[job.id] = job.as_json
|
117
|
+
end
|
118
|
+
|
119
|
+
def initial_jobs
|
120
|
+
all_jobs.select(&:initial?)
|
121
|
+
end
|
122
|
+
|
123
|
+
def find_job(id_or_klass)
|
124
|
+
id = if jobs.key?(id_or_klass)
|
125
|
+
id_or_klass
|
126
|
+
else
|
127
|
+
find_id_by_klass(id_or_klass)
|
128
|
+
end
|
129
|
+
|
130
|
+
get_job(id)
|
131
|
+
end
|
132
|
+
|
133
|
+
def get_job_hash(id)
|
134
|
+
jobs[id]
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def find_id_by_klass(klass)
|
140
|
+
finded = jobs.select do |_, job|
|
141
|
+
job[:klass].to_s == klass.to_s
|
142
|
+
end
|
143
|
+
|
144
|
+
raise 'Duplicat job detected' if finded.count > 1
|
145
|
+
finded.first.second[:id]
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Burst::WorkflowHelper
|
2
|
+
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
class JSONBWithIndifferentAccess
|
6
|
+
|
7
|
+
def self.dump(hash)
|
8
|
+
hash.as_json
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.load(hash)
|
12
|
+
hash ||= {}
|
13
|
+
hash = JSON.parse(hash) if hash.is_a? String
|
14
|
+
hash.with_indifferent_access
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
included do |_klass|
|
20
|
+
serialize :flow, JSONBWithIndifferentAccess
|
21
|
+
|
22
|
+
def first_job
|
23
|
+
all_jobs.min_by{|n| n.started_at || Time.now.to_i }
|
24
|
+
end
|
25
|
+
|
26
|
+
def last_job
|
27
|
+
all_jobs.max_by{|n| n.finished_at || 0 } if finished?
|
28
|
+
end
|
29
|
+
|
30
|
+
def started_at
|
31
|
+
first_job&.started_at
|
32
|
+
end
|
33
|
+
|
34
|
+
def finished_at
|
35
|
+
last_job&.finished_at
|
36
|
+
end
|
37
|
+
|
38
|
+
def configure(*args)
|
39
|
+
instance_exec *args, &self.class.configuration
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class_methods do
|
44
|
+
def define_flow_attributes(*keys)
|
45
|
+
keys.each do |key|
|
46
|
+
define_method key.to_sym do
|
47
|
+
return flow[key.to_s]
|
48
|
+
end
|
49
|
+
|
50
|
+
define_method "#{key}=".to_sym do |v|
|
51
|
+
return flow[key.to_s] = v
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def configure(&block)
|
57
|
+
@configuration = block
|
58
|
+
end
|
59
|
+
|
60
|
+
def configuration
|
61
|
+
@configuration
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
# class Test
|
69
|
+
# def self.configure &block
|
70
|
+
# @c = block
|
71
|
+
# end
|
72
|
+
|
73
|
+
# def self.c
|
74
|
+
# @c
|
75
|
+
# end
|
76
|
+
|
77
|
+
# def go
|
78
|
+
# puts "111111"
|
79
|
+
# end
|
80
|
+
|
81
|
+
# def run
|
82
|
+
# instance_eval &Test.c
|
83
|
+
# end
|
84
|
+
|
85
|
+
|
86
|
+
# end
|
data/lib/burst.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
require 'bundler/setup'
|
4
|
+
Bundler.require(:default)
|
5
|
+
|
6
|
+
require 'active_support/all'
|
7
|
+
require 'active_support/dependencies'
|
8
|
+
require 'active_record'
|
9
|
+
require 'active_job'
|
10
|
+
|
11
|
+
require 'pathname'
|
12
|
+
require 'securerandom'
|
13
|
+
|
14
|
+
require 'burst/configuration'
|
15
|
+
require 'burst/model'
|
16
|
+
require 'burst/builder'
|
17
|
+
require 'burst/manager'
|
18
|
+
require 'burst/job'
|
19
|
+
require 'burst/workflow_helper'
|
20
|
+
require 'burst/workflow'
|
21
|
+
require 'burst/worker'
|
22
|
+
|
23
|
+
module Burst
|
24
|
+
|
25
|
+
def self.root
|
26
|
+
Pathname.new(__FILE__).parent.parent
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.configuration
|
30
|
+
@configuration ||= Configuration.new
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.configure
|
34
|
+
yield configuration
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
data/spec/burst_spec.rb
ADDED