burstflow 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|