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