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