burstflow 0.1.0
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 +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