burstflow 0.1.1 → 0.2.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 +5 -5
- data/Gemfile +1 -3
- data/Gemfile.lock +119 -0
- data/burstflow.gemspec +10 -6
- data/config/database.yml +4 -3
- data/db/migrate/20180101000001_create_workflow.rb +1 -0
- data/db/schema.rb +13 -7
- data/lib/burstflow.rb +11 -0
- data/lib/burstflow/job.rb +102 -0
- data/lib/burstflow/job/callbacks.rb +55 -0
- data/lib/burstflow/job/exception.rb +8 -0
- data/lib/burstflow/job/initialization.rb +35 -0
- data/lib/{burst → burstflow/job}/model.rb +1 -3
- data/lib/burstflow/job/state.rb +125 -0
- data/lib/burstflow/manager.rb +123 -0
- data/lib/burstflow/railtie.rb +6 -0
- data/lib/burstflow/version.rb +3 -0
- data/lib/burstflow/worker.rb +59 -0
- data/lib/burstflow/workflow.rb +207 -0
- data/lib/burstflow/workflow/builder.rb +91 -0
- data/lib/burstflow/workflow/callbacks.rb +66 -0
- data/lib/{burst/workflow_helper.rb → burstflow/workflow/configuration.rb} +8 -39
- data/lib/burstflow/workflow/exception.rb +8 -0
- data/lib/generators/burstflow/install/install_generator.rb +22 -0
- data/lib/generators/burstflow/install/templates/create_workflow.rb +15 -0
- data/spec/builder_spec.rb +63 -0
- data/spec/{burst_spec.rb → burstflow_spec.rb} +1 -1
- data/spec/generators/install_generator_spec.rb +27 -0
- data/spec/job_spec.rb +18 -8
- data/spec/spec_helper.rb +4 -1
- data/spec/support/database_clean.rb +4 -1
- data/spec/workflow_spec.rb +397 -147
- metadata +45 -21
- data/db/migrate/20180101000001_create_workflow.rb +0 -13
- data/db/seeds.rb +0 -1
- data/lib/burst.rb +0 -37
- data/lib/burst/builder.rb +0 -48
- data/lib/burst/configuration.rb +0 -27
- data/lib/burst/job.rb +0 -187
- data/lib/burst/manager.rb +0 -79
- data/lib/burst/worker.rb +0 -42
- data/lib/burst/workflow.rb +0 -148
- data/spec/cases_spec.rb +0 -180
@@ -1,12 +1,10 @@
|
|
1
|
-
module
|
1
|
+
module Burstflow::Job::Model
|
2
2
|
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
5
|
included do |klass|
|
6
|
-
klass.include ActiveModel::Model
|
7
6
|
klass.include ActiveModel::Dirty
|
8
7
|
klass.include ActiveModel::Serialization
|
9
|
-
klass.extend ActiveModel::Callbacks
|
10
8
|
|
11
9
|
attr_accessor :model
|
12
10
|
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module Burstflow::Job::State
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
|
6
|
+
# mark job as enqueued when it is scheduled to queue
|
7
|
+
def enqueue!
|
8
|
+
raise Burstflow::Job::InternalError.new(self, "Can't enqueue: already enqueued") if enqueued?
|
9
|
+
self.enqueued_at = current_timestamp
|
10
|
+
self.started_at = nil
|
11
|
+
self.finished_at = nil
|
12
|
+
self.failed_at = nil
|
13
|
+
self.suspended_at = nil
|
14
|
+
self.resumed_at = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
# mark job as started when it is start performing
|
18
|
+
def start!
|
19
|
+
raise Burstflow::Job::InternalError.new(self, "Can't start: already started") if started?
|
20
|
+
raise Burstflow::Job::InternalError.new(self, "Can't start: not enqueued") if !enqueued?
|
21
|
+
self.started_at = current_timestamp
|
22
|
+
end
|
23
|
+
|
24
|
+
# mark job as finished when it is finish performing
|
25
|
+
def finish!
|
26
|
+
raise Burstflow::Job::InternalError.new(self, "Can't finish: already finished") if finished?
|
27
|
+
raise Burstflow::Job::InternalError.new(self, "Can't finish: not started") if !started?
|
28
|
+
self.finished_at = current_timestamp
|
29
|
+
end
|
30
|
+
|
31
|
+
# mark job as failed when it is failed
|
32
|
+
def fail! msg_or_exception
|
33
|
+
#raise Burstflow::Job::InternalError.new(self, "Can't fail: already failed") if failed?
|
34
|
+
#raise Burstflow::Job::InternalError.new(self, Can't fail: already finished") if finished?
|
35
|
+
raise Burstflow::Job::InternalError.new(self, "Can't fail: not started") if !started?
|
36
|
+
self.finished_at = self.failed_at = current_timestamp
|
37
|
+
|
38
|
+
context = {}
|
39
|
+
if msg_or_exception.is_a?(::Exception)
|
40
|
+
context[:message] = msg_or_exception.message
|
41
|
+
context[:klass] = msg_or_exception.class.to_s
|
42
|
+
context[:backtrace] = msg_or_exception.backtrace.first(10)
|
43
|
+
context[:cause] = msg_or_exception.cause.try(:inspect)
|
44
|
+
else
|
45
|
+
context[:message] = msg_or_exception
|
46
|
+
end
|
47
|
+
|
48
|
+
self.failure = context
|
49
|
+
end
|
50
|
+
|
51
|
+
# mark job as suspended
|
52
|
+
def suspend!
|
53
|
+
raise Burstflow::Job::InternalError.new(self, "Can't suspend: already suspended") if suspended?
|
54
|
+
raise Burstflow::Job::InternalError.new(self, "Can't suspend: not runnig") if !running?
|
55
|
+
self.suspended_at = current_timestamp
|
56
|
+
end
|
57
|
+
|
58
|
+
# mark job as resumed
|
59
|
+
def resume!
|
60
|
+
raise Burstflow::Job::InternalError.new(self, "Can't resume: already resumed") if resumed?
|
61
|
+
raise Burstflow::Job::InternalError.new(self, "Can't resume: not suspended") if !suspended?
|
62
|
+
self.resumed_at = current_timestamp
|
63
|
+
end
|
64
|
+
|
65
|
+
def enqueued?
|
66
|
+
!enqueued_at.nil?
|
67
|
+
end
|
68
|
+
|
69
|
+
def started?
|
70
|
+
!started_at.nil?
|
71
|
+
end
|
72
|
+
|
73
|
+
def finished?
|
74
|
+
!finished_at.nil?
|
75
|
+
end
|
76
|
+
|
77
|
+
def running?
|
78
|
+
started? && !finished? && !suspended?
|
79
|
+
end
|
80
|
+
|
81
|
+
def scheduled?
|
82
|
+
enqueued? && !finished? && !suspended?
|
83
|
+
end
|
84
|
+
|
85
|
+
def failed?
|
86
|
+
!failed_at.nil?
|
87
|
+
end
|
88
|
+
|
89
|
+
def suspended?
|
90
|
+
!suspended_at.nil? && !resumed?
|
91
|
+
end
|
92
|
+
|
93
|
+
def resumed?
|
94
|
+
!resumed_at.nil?
|
95
|
+
end
|
96
|
+
|
97
|
+
def succeeded?
|
98
|
+
finished? && !failed?
|
99
|
+
end
|
100
|
+
|
101
|
+
def ready_to_start?
|
102
|
+
!running? && !enqueued? && !finished? && !failed? && parents_succeeded?
|
103
|
+
end
|
104
|
+
|
105
|
+
def initial?
|
106
|
+
incoming.empty?
|
107
|
+
end
|
108
|
+
|
109
|
+
def parents_succeeded?
|
110
|
+
incoming.all? do |id|
|
111
|
+
workflow.job(id).succeeded?
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def current_timestamp
|
116
|
+
Time.now.to_i
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
class_methods do
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module Burstflow
|
2
|
+
class Manager
|
3
|
+
|
4
|
+
attr_accessor :workflow
|
5
|
+
|
6
|
+
def initialize(workflow)
|
7
|
+
@workflow = workflow
|
8
|
+
end
|
9
|
+
|
10
|
+
#workflow management
|
11
|
+
|
12
|
+
def start_workflow!
|
13
|
+
workflow.with_lock do
|
14
|
+
workflow.runnig!
|
15
|
+
|
16
|
+
workflow.initial_jobs.each do |job|
|
17
|
+
enqueue_job!(job)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def resume_workflow! job_id, data
|
23
|
+
workflow.with_lock do
|
24
|
+
workflow.resumed!
|
25
|
+
|
26
|
+
job = workflow.job(job_id)
|
27
|
+
resume_job!(job, data)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
#Mark job enqueued and enqueue it
|
33
|
+
def enqueue_job!(job)
|
34
|
+
job.run_callbacks :enqueue do
|
35
|
+
job.enqueue!
|
36
|
+
job.save! do
|
37
|
+
Burstflow::Worker.perform_later(workflow.id, job.id)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
#Enqueue job for resuming
|
43
|
+
def resume_job!(job, data)
|
44
|
+
job.save! do
|
45
|
+
Burstflow::Worker.perform_later(workflow.id, job.id, data)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
#Mark job suspended and forget it until resume
|
51
|
+
def suspend_job!(job)
|
52
|
+
job.run_callbacks :suspend do
|
53
|
+
job.suspend!
|
54
|
+
job.save! do
|
55
|
+
analyze_workflow_state(job)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
#Mark job finished and make further actions
|
62
|
+
def finish_job!(job)
|
63
|
+
job.finish!
|
64
|
+
job.save! do
|
65
|
+
analyze_workflow_state(job)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
#Mark job failed and make further actions
|
70
|
+
def fail_job!(job, exception)
|
71
|
+
job.run_callbacks :failure do
|
72
|
+
job.fail! exception
|
73
|
+
workflow.add_error(job)
|
74
|
+
workflow.save!
|
75
|
+
|
76
|
+
job.save! do
|
77
|
+
analyze_workflow_state(job)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
#Mark job finished or suspended depends on result or output
|
83
|
+
def job_performed!(job, result)
|
84
|
+
if result == Burstflow::Job::SUSPEND || job.output == Burstflow::Job::SUSPEND
|
85
|
+
suspend_job!(job)
|
86
|
+
else
|
87
|
+
finish_job!(job)
|
88
|
+
end
|
89
|
+
rescue => e
|
90
|
+
raise Burstflow::Workflow::InternalError.new(workflow, e.message)
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
#analyze job completition, current workflow state and perform futher actions
|
96
|
+
def analyze_workflow_state job
|
97
|
+
unless ActiveRecord::Base.connection.open_transactions > 0
|
98
|
+
raise Burstflow::Workflow::InternalError.new(workflow, "analyze_workflow_state must be called in transaction with lock!")
|
99
|
+
end
|
100
|
+
|
101
|
+
if job.succeeded? && job.outgoing.any? && !workflow.has_errors?
|
102
|
+
return enqueue_outgoing_jobs(job)
|
103
|
+
else
|
104
|
+
if workflow.has_scheduled_jobs?
|
105
|
+
#do nothing
|
106
|
+
#scheduled jobs will perform finish action
|
107
|
+
else
|
108
|
+
workflow.complete!
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
#enqueue outgoing jobs if all requirements are met
|
114
|
+
def enqueue_outgoing_jobs(job)
|
115
|
+
job.outgoing.each do |job_id|
|
116
|
+
out = workflow.job(job_id)
|
117
|
+
|
118
|
+
enqueue_job!(out) if out.ready_to_start?
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'active_job'
|
2
|
+
|
3
|
+
class Burstflow::Worker < ::ActiveJob::Base
|
4
|
+
|
5
|
+
attr_reader :workflow, :job
|
6
|
+
|
7
|
+
rescue_from(Exception) do |exception|
|
8
|
+
@manager.fail_job!(job, exception)
|
9
|
+
end
|
10
|
+
|
11
|
+
rescue_from(Burstflow::Job::InternalError) do |exception|
|
12
|
+
@manager.fail_job!(exception.job, exception)
|
13
|
+
end
|
14
|
+
|
15
|
+
rescue_from(Burstflow::Workflow::InternalError) do |exception|
|
16
|
+
exception.workflow.add_error(exception)
|
17
|
+
exception.workflow.save!
|
18
|
+
end
|
19
|
+
|
20
|
+
before_perform do
|
21
|
+
workflow_id, job_id, resume_data = arguments
|
22
|
+
|
23
|
+
@workflow = Burstflow::Workflow.find(workflow_id)
|
24
|
+
@job = @workflow.job(job_id)
|
25
|
+
@manager = @workflow.manager
|
26
|
+
|
27
|
+
set_incoming_payloads(job)
|
28
|
+
end
|
29
|
+
|
30
|
+
def perform(workflow_id, job_id, resume_data = nil)
|
31
|
+
result = if resume_data.nil?
|
32
|
+
job.start!
|
33
|
+
job.save!
|
34
|
+
|
35
|
+
job.perform_now
|
36
|
+
else
|
37
|
+
job.resume!
|
38
|
+
job.save!
|
39
|
+
|
40
|
+
job.resume_now(resume_data)
|
41
|
+
end
|
42
|
+
|
43
|
+
@manager.job_performed!(job, result)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def set_incoming_payloads job
|
49
|
+
job.payloads = job.incoming.map do |job_id|
|
50
|
+
incoming = workflow.job(job_id)
|
51
|
+
{
|
52
|
+
id: incoming.id,
|
53
|
+
class: incoming.klass.to_s,
|
54
|
+
value: incoming.output
|
55
|
+
}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
require 'burstflow/manager'
|
4
|
+
|
5
|
+
module Burstflow
|
6
|
+
class Workflow < ActiveRecord::Base
|
7
|
+
require 'burstflow/workflow/exception'
|
8
|
+
require 'burstflow/workflow/builder'
|
9
|
+
require 'burstflow/workflow/configuration'
|
10
|
+
require 'burstflow/workflow/callbacks'
|
11
|
+
|
12
|
+
self.table_name_prefix = 'burstflow_'
|
13
|
+
|
14
|
+
INITIAL = 'initial'.freeze
|
15
|
+
RUNNING = 'running'.freeze
|
16
|
+
FINISHED = 'finished'.freeze
|
17
|
+
FAILED = 'failed'.freeze
|
18
|
+
SUSPENDED = 'suspended'.freeze
|
19
|
+
|
20
|
+
STATUSES = [INITIAL, RUNNING, FINISHED, FAILED, SUSPENDED].freeze
|
21
|
+
|
22
|
+
include Burstflow::Workflow::Configuration
|
23
|
+
include Burstflow::Workflow::Callbacks
|
24
|
+
|
25
|
+
attr_accessor :manager, :cache
|
26
|
+
define_flow_attributes :jobs_config, :failures
|
27
|
+
|
28
|
+
after_initialize do
|
29
|
+
@cache = {}
|
30
|
+
|
31
|
+
self.status ||= INITIAL
|
32
|
+
self.id ||= SecureRandom.uuid
|
33
|
+
self.jobs_config ||= {}.with_indifferent_access
|
34
|
+
self.failures ||= []
|
35
|
+
|
36
|
+
@manager = Burstflow::Manager.new(self)
|
37
|
+
end
|
38
|
+
|
39
|
+
STATUSES.each do |name|
|
40
|
+
define_method "#{name}?".to_sym do
|
41
|
+
self.status == name
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def attributes
|
46
|
+
{
|
47
|
+
id: self.id,
|
48
|
+
jobs_config: self.jobs_config,
|
49
|
+
type: self.class.to_s,
|
50
|
+
status: status,
|
51
|
+
failures: failures
|
52
|
+
}
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.build(*args)
|
56
|
+
new.tap do |wf|
|
57
|
+
builder = Burstflow::Workflow::Builder.new(wf, *args, &configuration)
|
58
|
+
wf.flow = {'jobs_config' => builder.as_json}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def reload(*)
|
63
|
+
self.cache = {}
|
64
|
+
super
|
65
|
+
end
|
66
|
+
|
67
|
+
def start!
|
68
|
+
manager.start_workflow!
|
69
|
+
self
|
70
|
+
end
|
71
|
+
|
72
|
+
def resume!(job_id, data)
|
73
|
+
manager.resume_workflow!(job_id, data)
|
74
|
+
self
|
75
|
+
end
|
76
|
+
|
77
|
+
def jobs
|
78
|
+
Enumerator.new do |y|
|
79
|
+
jobs_config.keys.each do |id|
|
80
|
+
y << job(id)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def job_hash(id)
|
86
|
+
jobs_config[id].deep_dup
|
87
|
+
end
|
88
|
+
|
89
|
+
def job(id)
|
90
|
+
Burstflow::Job.from_hash(self, job_hash(id))
|
91
|
+
end
|
92
|
+
|
93
|
+
def set_job(job)
|
94
|
+
jobs_config[job.id] = job.as_json
|
95
|
+
end
|
96
|
+
|
97
|
+
def initial_jobs
|
98
|
+
cache[:initial_jobs] ||= jobs.select(&:initial?)
|
99
|
+
end
|
100
|
+
|
101
|
+
def add_error job_orexception
|
102
|
+
context = {
|
103
|
+
created_at: Time.now.to_i
|
104
|
+
}
|
105
|
+
if job_orexception.is_a?(::Exception)
|
106
|
+
context[:message] = job_orexception.message
|
107
|
+
context[:klass] = job_orexception.class.to_s
|
108
|
+
context[:backtrace] = job_orexception.backtrace.first(10)
|
109
|
+
context[:cause] = job_orexception.cause
|
110
|
+
else
|
111
|
+
context[:job] = job_orexception.id
|
112
|
+
end
|
113
|
+
|
114
|
+
failures.push(context)
|
115
|
+
end
|
116
|
+
|
117
|
+
def has_errors?
|
118
|
+
failures.any?
|
119
|
+
end
|
120
|
+
|
121
|
+
def has_scheduled_jobs?
|
122
|
+
cache[:has_scheduled_jobs] ||= jobs.any? do |job|
|
123
|
+
job.scheduled? || (job.initial? && !job.enqueued?)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def has_suspended_jobs?
|
128
|
+
cache[:has_suspended_jobs] ||= jobs.any?(&:suspended?)
|
129
|
+
end
|
130
|
+
|
131
|
+
def complete!
|
132
|
+
if has_errors?
|
133
|
+
failed!
|
134
|
+
elsif has_suspended_jobs?
|
135
|
+
suspended!
|
136
|
+
else
|
137
|
+
finished!
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def first_job
|
142
|
+
all_jobs.min_by{|n| n.started_at || Time.now.to_i }
|
143
|
+
end
|
144
|
+
|
145
|
+
def last_job
|
146
|
+
all_jobs.max_by{|n| n.finished_at || 0 } if finished?
|
147
|
+
end
|
148
|
+
|
149
|
+
def started_at
|
150
|
+
first_job&.started_at
|
151
|
+
end
|
152
|
+
|
153
|
+
def finished_at
|
154
|
+
last_job&.finished_at
|
155
|
+
end
|
156
|
+
|
157
|
+
def runnig!
|
158
|
+
raise InternalError.new(self, "Can't start: workflow already running") if (running? || suspended?)
|
159
|
+
raise InternalError.new(self, "Can't start: workflow already failed") if failed?
|
160
|
+
raise InternalError.new(self, "Can't start: workflow already finished") if finished?
|
161
|
+
self.status = RUNNING
|
162
|
+
save!
|
163
|
+
end
|
164
|
+
|
165
|
+
def failed!
|
166
|
+
run_callbacks :failure do
|
167
|
+
raise InternalError.new(self, "Can't fail: workflow already failed") if failed?
|
168
|
+
raise InternalError.new(self, "Can't fail: workflow already finished") if finished?
|
169
|
+
raise InternalError.new(self, "Can't fail: workflow in not runnig") if !(running? || suspended?)
|
170
|
+
self.status = FAILED
|
171
|
+
save!
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def finished!
|
176
|
+
run_callbacks :finish do
|
177
|
+
raise InternalError.new(self, "Can't finish: workflow already finished") if finished?
|
178
|
+
raise InternalError.new(self, "Can't finish: workflow already failed") if failed?
|
179
|
+
raise InternalError.new(self, "Can't finish: workflow in not runnig") if !running?
|
180
|
+
self.status = FINISHED
|
181
|
+
save!
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def suspended!
|
186
|
+
run_callbacks :suspend do
|
187
|
+
raise InternalError.new(self, "Can't suspend: workflow already finished") if finished?
|
188
|
+
raise InternalError.new(self, "Can't suspend: workflow already failed") if failed?
|
189
|
+
raise InternalError.new(self, "Can't suspend: workflow in not runnig") if !running?
|
190
|
+
self.status = SUSPENDED
|
191
|
+
save!
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def resumed!
|
196
|
+
run_callbacks :resume do
|
197
|
+
raise InternalError.new(self, "Can't resume: workflow already running") if running?
|
198
|
+
raise InternalError.new(self, "Can't resume: workflow already finished") if finished?
|
199
|
+
raise InternalError.new(self, "Can't resume: workflow already failed") if failed?
|
200
|
+
raise InternalError.new(self, "Can't resume: workflow in not suspended") if !suspended?
|
201
|
+
self.status = RUNNING
|
202
|
+
save!
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
207
|
+
end
|