dynflow 0.5.1 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/dynflow/action.rb +2 -0
- data/lib/dynflow/action/polling.rb +74 -14
- data/lib/dynflow/action/progress.rb +32 -31
- data/lib/dynflow/execution_plan.rb +17 -12
- data/lib/dynflow/execution_plan/steps/abstract.rb +34 -11
- data/lib/dynflow/execution_plan/steps/abstract_flow_step.rb +1 -6
- data/lib/dynflow/execution_plan/steps/plan_step.rb +1 -3
- data/lib/dynflow/executors/parallel/execution_plan_manager.rb +0 -7
- data/lib/dynflow/executors/parallel/sequential_manager.rb +0 -2
- data/lib/dynflow/persistence_adapters/sequel.rb +1 -1
- data/lib/dynflow/persistence_adapters/sequel_migrations/002_incremental_progress.rb +8 -0
- data/lib/dynflow/testing.rb +1 -1
- data/lib/dynflow/testing/managed_clock.rb +21 -5
- data/lib/dynflow/version.rb +1 -1
- data/lib/dynflow/web_console.rb +3 -3
- data/test/action_test.rb +161 -0
- data/test/executor_test.rb +5 -5
- data/test/support/code_workflow_example.rb +7 -23
- data/test/test_helper.rb +0 -2
- data/test/testing_test.rb +13 -10
- data/web/views/flow_step.erb +1 -1
- metadata +3 -2
data/lib/dynflow/action.rb
CHANGED
@@ -6,44 +6,104 @@ module Dynflow
|
|
6
6
|
def run(event = nil)
|
7
7
|
case event
|
8
8
|
when nil
|
9
|
-
|
10
|
-
|
9
|
+
if external_task
|
10
|
+
resume_external_action
|
11
|
+
else
|
12
|
+
initiate_external_action
|
13
|
+
end
|
11
14
|
when Poll
|
12
|
-
|
13
|
-
suspend_and_ping unless done?
|
15
|
+
poll_external_task_with_rescue
|
14
16
|
else
|
15
17
|
raise "unrecognized event #{event}"
|
16
18
|
end
|
17
19
|
end
|
18
20
|
|
19
|
-
def
|
21
|
+
def done?
|
20
22
|
raise NotImplementedError
|
21
23
|
end
|
22
24
|
|
23
|
-
def
|
25
|
+
def invoke_external_task
|
24
26
|
raise NotImplementedError
|
25
27
|
end
|
26
28
|
|
27
|
-
|
28
|
-
|
29
|
-
def invoke_external_task
|
29
|
+
def poll_external_task
|
30
30
|
raise NotImplementedError
|
31
31
|
end
|
32
32
|
|
33
|
+
# External task data. It should return nil when the task has not
|
34
|
+
# been triggered yet.
|
35
|
+
def external_task
|
36
|
+
output[:task]
|
37
|
+
end
|
38
|
+
|
33
39
|
def external_task=(external_task_data)
|
34
|
-
|
40
|
+
output[:task] = external_task_data
|
35
41
|
end
|
36
42
|
|
37
|
-
|
38
|
-
|
43
|
+
# What is the trend in waiting for next polling event. It allows
|
44
|
+
# to strart with frequent polling, but slow down once it's clear this
|
45
|
+
# task will take some time: the idea is we don't care that much in finishing
|
46
|
+
# few seconds sooner, when the task takes orders of minutes/hours. It allows
|
47
|
+
# not overwhelming the backend-servers with useless requests.
|
48
|
+
# By default, it switches to next interval after +attempts_before_next_interval+ number
|
49
|
+
# of attempts
|
50
|
+
def poll_intervals
|
51
|
+
[0.5, 1, 2, 4, 8, 16]
|
52
|
+
end
|
53
|
+
|
54
|
+
def attempts_before_next_interval
|
55
|
+
5
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns the time to wait between two polling intervals.
|
59
|
+
def poll_interval
|
60
|
+
interval_level = poll_attempts[:total]/attempts_before_next_interval
|
61
|
+
poll_intervals[interval_level] || poll_intervals.last
|
62
|
+
end
|
63
|
+
|
64
|
+
# How man times in row should we retry the polling before giving up
|
65
|
+
def poll_max_retries
|
66
|
+
3
|
67
|
+
end
|
68
|
+
|
69
|
+
def initiate_external_action
|
70
|
+
self.external_task = invoke_external_task
|
71
|
+
suspend_and_ping unless done?
|
72
|
+
end
|
73
|
+
|
74
|
+
def resume_external_action
|
75
|
+
poll_external_task_with_rescue
|
76
|
+
rescue
|
77
|
+
initiate_external_action
|
39
78
|
end
|
40
79
|
|
41
80
|
def suspend_and_ping
|
42
81
|
suspend { |suspended_action| world.clock.ping suspended_action, poll_interval, Poll }
|
43
82
|
end
|
44
83
|
|
45
|
-
def
|
46
|
-
|
84
|
+
def poll_external_task_with_rescue
|
85
|
+
poll_attempts[:total] += 1
|
86
|
+
self.external_task = poll_external_task
|
87
|
+
poll_attempts[:failed] = 0
|
88
|
+
suspend_and_ping unless done?
|
89
|
+
rescue => error
|
90
|
+
poll_attempts[:failed] += 1
|
91
|
+
rescue_poll_external_task(error)
|
47
92
|
end
|
93
|
+
|
94
|
+
def poll_attempts
|
95
|
+
output[:poll_attempts] ||= { total: 0, failed: 0 }
|
96
|
+
end
|
97
|
+
|
98
|
+
def rescue_poll_external_task(error)
|
99
|
+
if poll_attempts[:failed] < poll_max_retries
|
100
|
+
action_logger.warn("Polling failed, attempt no. #{poll_attempts[:failed]}, retrying in #{poll_interval}")
|
101
|
+
action_logger.warn(error)
|
102
|
+
suspend_and_ping
|
103
|
+
else
|
104
|
+
raise error
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
48
108
|
end
|
49
109
|
end
|
@@ -10,6 +10,37 @@ module Dynflow
|
|
10
10
|
# the progress is 1 for success/skipped actions and 0 for errorneous ones.
|
11
11
|
module Action::Progress
|
12
12
|
|
13
|
+
class Calculate < Middleware
|
14
|
+
|
15
|
+
def run(*args)
|
16
|
+
with_progress_calculation(*args) do
|
17
|
+
[action.run_progress, action.run_progress_weight]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def finalize(*args)
|
22
|
+
with_progress_calculation(*args) do
|
23
|
+
[action.finalize_progress, action.finalize_progress_weight]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def with_progress_calculation(*args)
|
30
|
+
pass(*args)
|
31
|
+
ensure
|
32
|
+
begin
|
33
|
+
action.calculated_progress = yield
|
34
|
+
rescue => error
|
35
|
+
# we don't want progress calculation to cause fail of the whole process
|
36
|
+
# TODO: introduce post-execute state for handling issues with additional
|
37
|
+
# calculations after the step is run
|
38
|
+
action.action_logger.error('Error in progress calculation')
|
39
|
+
action.action_logger.error(error)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
13
44
|
def run_progress
|
14
45
|
0.5
|
15
46
|
end
|
@@ -26,37 +57,7 @@ module Dynflow
|
|
26
57
|
1
|
27
58
|
end
|
28
59
|
|
29
|
-
|
30
|
-
# variants instead
|
31
|
-
def progress_done
|
32
|
-
case self.state
|
33
|
-
when :success, :skipped
|
34
|
-
1
|
35
|
-
when :running, :suspended
|
36
|
-
case phase
|
37
|
-
when Action::Run
|
38
|
-
run_progress
|
39
|
-
when Action::Finalize
|
40
|
-
finalize_progress
|
41
|
-
else
|
42
|
-
raise 'Calculating progress for this phase is not supported'
|
43
|
-
end
|
44
|
-
else
|
45
|
-
0
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
def progress_weight
|
50
|
-
case phase
|
51
|
-
when Action::Run
|
52
|
-
run_progress_weight
|
53
|
-
when Action::Finalize
|
54
|
-
finalize_progress_weight
|
55
|
-
else
|
56
|
-
raise 'Calculating progress for this phase is not supported'
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
|
+
attr_accessor :calculated_progress
|
60
61
|
end
|
61
62
|
end
|
62
63
|
|
@@ -37,7 +37,7 @@ module Dynflow
|
|
37
37
|
steps = {},
|
38
38
|
started_at = nil,
|
39
39
|
ended_at = nil,
|
40
|
-
execution_time =
|
40
|
+
execution_time = nil,
|
41
41
|
real_time = 0.0)
|
42
42
|
|
43
43
|
@id = Type! id, String
|
@@ -48,8 +48,8 @@ module Dynflow
|
|
48
48
|
@root_plan_step = root_plan_step
|
49
49
|
@started_at = Type! started_at, Time, NilClass
|
50
50
|
@ended_at = Type! ended_at, Time, NilClass
|
51
|
-
@execution_time = Type! execution_time,
|
52
|
-
@real_time = Type! real_time,
|
51
|
+
@execution_time = Type! execution_time, Numeric, NilClass
|
52
|
+
@real_time = Type! real_time, Numeric
|
53
53
|
|
54
54
|
steps.all? do |k, v|
|
55
55
|
Type! k, Integer
|
@@ -68,8 +68,9 @@ module Dynflow
|
|
68
68
|
when :planning
|
69
69
|
@started_at = Time.now
|
70
70
|
when :stopped
|
71
|
-
@ended_at
|
72
|
-
@real_time
|
71
|
+
@ended_at = Time.now
|
72
|
+
@real_time = @ended_at - @started_at
|
73
|
+
@execution_time = compute_execution_time
|
73
74
|
else
|
74
75
|
# ignore
|
75
76
|
end
|
@@ -78,10 +79,6 @@ module Dynflow
|
|
78
79
|
self.save
|
79
80
|
end
|
80
81
|
|
81
|
-
def update_execution_time(execution_time)
|
82
|
-
@execution_time += execution_time
|
83
|
-
end
|
84
|
-
|
85
82
|
def result
|
86
83
|
all_steps = steps.values
|
87
84
|
if all_steps.any? { |step| step.state == :error }
|
@@ -205,6 +202,7 @@ module Dynflow
|
|
205
202
|
|
206
203
|
def add_run_step(action)
|
207
204
|
add_step(Steps::RunStep, action.class, action.id).tap do |step|
|
205
|
+
step.progress_weight = action.run_progress_weight
|
208
206
|
@dependency_graph.add_dependencies(step, action)
|
209
207
|
current_run_flow.add_and_resolve(@dependency_graph, Flows::Atom.new(step.id))
|
210
208
|
end
|
@@ -212,6 +210,7 @@ module Dynflow
|
|
212
210
|
|
213
211
|
def add_finalize_step(action)
|
214
212
|
add_step(Steps::FinalizeStep, action.class, action.id).tap do |step|
|
213
|
+
step.progress_weight = action.finalize_progress_weight
|
215
214
|
finalize_flow << Flows::Atom.new(step.id)
|
216
215
|
end
|
217
216
|
end
|
@@ -252,14 +251,20 @@ module Dynflow
|
|
252
251
|
hash[:real_time])
|
253
252
|
end
|
254
253
|
|
254
|
+
def compute_execution_time
|
255
|
+
self.steps.values.reduce(0) do |execution_time, step|
|
256
|
+
execution_time + (step.execution_time || 0)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
255
260
|
# @return [0..1] the percentage of the progress. See Action::Progress for more
|
256
261
|
# info
|
257
262
|
def progress
|
258
263
|
flow_step_ids = run_flow.all_step_ids + finalize_flow.all_step_ids
|
259
264
|
plan_done, plan_total = flow_step_ids.reduce([0.0, 0]) do |(done, total), step_id|
|
260
|
-
|
261
|
-
[done + (
|
262
|
-
total +
|
265
|
+
step = self.steps[step_id]
|
266
|
+
[done + (step.progress_done * step.progress_weight),
|
267
|
+
total + step.progress_weight]
|
263
268
|
end
|
264
269
|
plan_total > 0 ? (plan_done / plan_total) : 1
|
265
270
|
end
|
@@ -18,7 +18,9 @@ module Dynflow
|
|
18
18
|
started_at = nil,
|
19
19
|
ended_at = nil,
|
20
20
|
execution_time = 0.0,
|
21
|
-
real_time = 0.0
|
21
|
+
real_time = 0.0,
|
22
|
+
progress_done = nil,
|
23
|
+
progress_weight = nil)
|
22
24
|
|
23
25
|
@id = id || raise(ArgumentError, 'missing id')
|
24
26
|
@execution_plan_id = Type! execution_plan_id, String
|
@@ -26,8 +28,11 @@ module Dynflow
|
|
26
28
|
@error = Type! error, ExecutionPlan::Steps::Error, NilClass
|
27
29
|
@started_at = Type! started_at, Time, NilClass
|
28
30
|
@ended_at = Type! ended_at, Time, NilClass
|
29
|
-
@execution_time = Type! execution_time,
|
30
|
-
@real_time = Type! real_time,
|
31
|
+
@execution_time = Type! execution_time, Numeric
|
32
|
+
@real_time = Type! real_time, Numeric
|
33
|
+
|
34
|
+
@progress_done = Type! progress_done, Numeric, NilClass
|
35
|
+
@progress_weight = Type! progress_weight, Numeric, NilClass
|
31
36
|
|
32
37
|
self.state = state.to_sym
|
33
38
|
|
@@ -76,16 +81,31 @@ module Dynflow
|
|
76
81
|
started_at: time_to_str(started_at),
|
77
82
|
ended_at: time_to_str(ended_at),
|
78
83
|
execution_time: execution_time,
|
79
|
-
real_time: real_time
|
84
|
+
real_time: real_time,
|
85
|
+
progress_done: progress_done,
|
86
|
+
progress_weight: progress_weight
|
87
|
+
end
|
88
|
+
|
89
|
+
def progress_done
|
90
|
+
default_progress_done || @progress_done || 0
|
80
91
|
end
|
81
92
|
|
82
|
-
#
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
93
|
+
# in specific states it's clear what progress the step is in
|
94
|
+
def default_progress_done
|
95
|
+
case self.state
|
96
|
+
when :success, :skipped
|
97
|
+
1
|
98
|
+
when :pending
|
99
|
+
0
|
100
|
+
end
|
87
101
|
end
|
88
102
|
|
103
|
+
def progress_weight
|
104
|
+
@progress_weight || 0 # 0 means not calculated yet
|
105
|
+
end
|
106
|
+
|
107
|
+
attr_writer :progress_weight # to allow setting the weight from planning
|
108
|
+
|
89
109
|
# @return [Action] in presentation mode, intended for retrieving: progress information,
|
90
110
|
# details, human outputs, etc.
|
91
111
|
def action(execution_plan)
|
@@ -108,16 +128,19 @@ module Dynflow
|
|
108
128
|
string_to_time(hash[:started_at]),
|
109
129
|
string_to_time(hash[:ended_at]),
|
110
130
|
hash[:execution_time],
|
111
|
-
hash[:real_time]
|
131
|
+
hash[:real_time],
|
132
|
+
hash[:progress_done],
|
133
|
+
hash[:progress_weight]
|
112
134
|
end
|
113
135
|
|
114
136
|
private
|
115
137
|
|
116
|
-
def
|
138
|
+
def with_meta_calculation(action, &block)
|
117
139
|
start = Time.now
|
118
140
|
@started_at ||= start
|
119
141
|
block.call
|
120
142
|
ensure
|
143
|
+
@progress_done, @progress_weight = action.calculated_progress
|
121
144
|
@ended_at = Time.now
|
122
145
|
@execution_time += @ended_at - start
|
123
146
|
@real_time = @ended_at - @started_at
|
@@ -5,7 +5,7 @@ module Dynflow
|
|
5
5
|
def execute(*args)
|
6
6
|
return self if [:skipped, :success].include? self.state
|
7
7
|
open_action do |action|
|
8
|
-
|
8
|
+
with_meta_calculation(action) do
|
9
9
|
action.execute(*args)
|
10
10
|
end
|
11
11
|
end
|
@@ -15,11 +15,6 @@ module Dynflow
|
|
15
15
|
self.class.from_hash(to_hash, execution_plan_id, world)
|
16
16
|
end
|
17
17
|
|
18
|
-
def progress
|
19
|
-
action = persistence.load_action(self)
|
20
|
-
[action.progress_done, action.progress_weight]
|
21
|
-
end
|
22
|
-
|
23
18
|
private
|
24
19
|
|
25
20
|
def open_action
|
@@ -50,12 +50,10 @@ module Dynflow
|
|
50
50
|
action = action_class.new(attributes, execution_plan.world)
|
51
51
|
persistence.save_action(execution_plan_id, action)
|
52
52
|
|
53
|
-
|
53
|
+
with_meta_calculation(action) do
|
54
54
|
action.execute(*args)
|
55
55
|
end
|
56
56
|
|
57
|
-
execution_plan.update_execution_time execution_time
|
58
|
-
|
59
57
|
persistence.save_action(execution_plan_id, action)
|
60
58
|
return action
|
61
59
|
end
|
@@ -51,15 +51,8 @@ module Dynflow
|
|
51
51
|
execution_plan.steps[step.id] = step
|
52
52
|
suspended, work = @running_steps_manager.done(step)
|
53
53
|
unless suspended
|
54
|
-
execution_plan.update_execution_time step.execution_time
|
55
54
|
work = compute_next_from_step.call step
|
56
55
|
end
|
57
|
-
# TODO: can be probably disabled to improve
|
58
|
-
# performance, execution time will not be updated,
|
59
|
-
# maybe more - check on the other side, it allows
|
60
|
-
# us to use persistence adapter for hooking into
|
61
|
-
# the running process.
|
62
|
-
execution_plan.save
|
63
56
|
work
|
64
57
|
end),
|
65
58
|
(on Work::Finalize do
|
@@ -25,7 +25,7 @@ module Dynflow
|
|
25
25
|
|
26
26
|
META_DATA = { execution_plan: %w(state result started_at ended_at real_time execution_time),
|
27
27
|
action: [],
|
28
|
-
step: %w(state started_at ended_at real_time execution_time action_id) }
|
28
|
+
step: %w(state started_at ended_at real_time execution_time action_id progress_done progress_weight) }
|
29
29
|
|
30
30
|
def initialize(db_path)
|
31
31
|
@db = initialize_db db_path
|
data/lib/dynflow/testing.rb
CHANGED
@@ -1,22 +1,38 @@
|
|
1
1
|
module Dynflow
|
2
2
|
module Testing
|
3
3
|
class ManagedClock
|
4
|
+
|
5
|
+
attr_reader :pending_pings
|
6
|
+
|
7
|
+
include Algebrick::Types
|
8
|
+
Timer = Algebrick.type do
|
9
|
+
fields! who: Object, # to ping back
|
10
|
+
when: type { variants Time, Numeric }, # to deliver
|
11
|
+
what: Maybe[Object], # to send
|
12
|
+
where: Symbol # it should be delivered, which method
|
13
|
+
end
|
14
|
+
|
15
|
+
module Timer
|
16
|
+
include Clock::Timer
|
17
|
+
end
|
18
|
+
|
4
19
|
def initialize
|
5
|
-
@
|
20
|
+
@pending_pings = []
|
6
21
|
end
|
7
22
|
|
8
23
|
def ping(who, time, with_what = nil, where = :<<)
|
9
|
-
|
24
|
+
with = with_what.nil? ? None : Some[Object][with_what]
|
25
|
+
@pending_pings << Timer[who, time, with, where]
|
10
26
|
end
|
11
27
|
|
12
28
|
def progress
|
13
|
-
copy = @
|
29
|
+
copy = @pending_pings.dup
|
14
30
|
clear
|
15
|
-
copy.each { |
|
31
|
+
copy.each { |ping| ping.apply }
|
16
32
|
end
|
17
33
|
|
18
34
|
def clear
|
19
|
-
@
|
35
|
+
@pending_pings.clear
|
20
36
|
end
|
21
37
|
end
|
22
38
|
end
|
data/lib/dynflow/version.rb
CHANGED
data/lib/dynflow/web_console.rb
CHANGED
@@ -126,11 +126,11 @@ module Dynflow
|
|
126
126
|
end
|
127
127
|
end
|
128
128
|
|
129
|
-
def progress_width(
|
130
|
-
if
|
129
|
+
def progress_width(step)
|
130
|
+
if step.state == :error
|
131
131
|
100 # we want to show the red bar in full width
|
132
132
|
else
|
133
|
-
|
133
|
+
step.progress_done * 100
|
134
134
|
end
|
135
135
|
end
|
136
136
|
|
data/test/action_test.rb
CHANGED
@@ -91,5 +91,166 @@ module Dynflow
|
|
91
91
|
end
|
92
92
|
end
|
93
93
|
|
94
|
+
describe 'polling action' do
|
95
|
+
CWE = Support::CodeWorkflowExample
|
96
|
+
include Dynflow::Testing
|
97
|
+
|
98
|
+
class ExternalService
|
99
|
+
def invoke(args)
|
100
|
+
reset!
|
101
|
+
end
|
102
|
+
|
103
|
+
def poll(id)
|
104
|
+
raise 'fail' if @current_state[:failing]
|
105
|
+
@current_state[:progress] += 10
|
106
|
+
return @current_state
|
107
|
+
end
|
108
|
+
|
109
|
+
def reset!
|
110
|
+
@current_state = { task_id: 123, progress: 0 }
|
111
|
+
end
|
112
|
+
|
113
|
+
def will_fail
|
114
|
+
@current_state[:failing] = true
|
115
|
+
end
|
116
|
+
|
117
|
+
def wont_fail
|
118
|
+
@current_state.delete(:failing)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
class TestPollingAction < Dynflow::Action
|
123
|
+
|
124
|
+
class Config
|
125
|
+
attr_accessor :external_service, :poll_max_retries,
|
126
|
+
:poll_intervals, :attempts_before_next_interval
|
127
|
+
|
128
|
+
def initialize
|
129
|
+
@external_service = ExternalService.new
|
130
|
+
@poll_max_retries = 2
|
131
|
+
@poll_intervals = [0.5, 1]
|
132
|
+
@attempts_before_next_interval = 2
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
include Dynflow::Action::Polling
|
137
|
+
|
138
|
+
def invoke_external_task
|
139
|
+
external_service.invoke(input[:task_args])
|
140
|
+
end
|
141
|
+
|
142
|
+
def poll_external_task
|
143
|
+
external_service.poll(external_task[:task_id])
|
144
|
+
end
|
145
|
+
|
146
|
+
def done?
|
147
|
+
external_task && external_task[:progress] >= 100
|
148
|
+
end
|
149
|
+
|
150
|
+
def poll_max_retries
|
151
|
+
self.class.config.poll_max_retries
|
152
|
+
end
|
153
|
+
|
154
|
+
def poll_intervals
|
155
|
+
self.class.config.poll_intervals
|
156
|
+
end
|
157
|
+
|
158
|
+
def attempts_before_next_interval
|
159
|
+
self.class.config.attempts_before_next_interval
|
160
|
+
end
|
161
|
+
|
162
|
+
class << self
|
163
|
+
def config
|
164
|
+
@config ||= Config.new
|
165
|
+
end
|
166
|
+
|
167
|
+
attr_writer :config
|
168
|
+
end
|
169
|
+
|
170
|
+
def external_service
|
171
|
+
self.class.config.external_service
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
let(:plan) do
|
176
|
+
create_and_plan_action TestPollingAction, { task_args: 'do something' }
|
177
|
+
end
|
178
|
+
|
179
|
+
before do
|
180
|
+
TestPollingAction.config = TestPollingAction::Config.new
|
181
|
+
end
|
182
|
+
|
183
|
+
def next_ping(action)
|
184
|
+
action.world.clock.pending_pings.first
|
185
|
+
end
|
186
|
+
|
187
|
+
it 'initiates the external task' do
|
188
|
+
action = run_action plan
|
189
|
+
|
190
|
+
action.output[:task][:task_id].must_equal 123
|
191
|
+
end
|
192
|
+
|
193
|
+
it 'polls till the task is done' do
|
194
|
+
action = run_action plan
|
195
|
+
|
196
|
+
9.times { progress_action_time action }
|
197
|
+
action.done?.must_equal false
|
198
|
+
next_ping(action).wont_be_nil
|
199
|
+
action.state.must_equal :suspended
|
200
|
+
|
201
|
+
progress_action_time action
|
202
|
+
action.done?.must_equal true
|
203
|
+
next_ping(action).must_be_nil
|
204
|
+
action.state.must_equal :success
|
205
|
+
end
|
206
|
+
|
207
|
+
it 'tries to poll for the old task when resuming' do
|
208
|
+
action = run_action plan
|
209
|
+
action.output[:task][:progress].must_equal 0
|
210
|
+
run_action action
|
211
|
+
action.output[:task][:progress].must_equal 10
|
212
|
+
end
|
213
|
+
|
214
|
+
it 'invokes the external task again when polling on the old one fails' do
|
215
|
+
action = run_action plan
|
216
|
+
action.world.action_logger.level = 3
|
217
|
+
action.external_service.will_fail
|
218
|
+
action.output[:task][:progress].must_equal 0
|
219
|
+
run_action action
|
220
|
+
action.output[:task][:progress].must_equal 0
|
221
|
+
end
|
222
|
+
|
223
|
+
it 'tolerates some failure while polling' do
|
224
|
+
action = run_action plan
|
225
|
+
action.external_service.will_fail
|
226
|
+
action.world.action_logger.level = 4
|
227
|
+
|
228
|
+
TestPollingAction.config.poll_max_retries = 3
|
229
|
+
(1..2).each do |attempt|
|
230
|
+
progress_action_time action
|
231
|
+
action.poll_attempts[:failed].must_equal attempt
|
232
|
+
next_ping(action).wont_be_nil
|
233
|
+
action.state.must_equal :suspended
|
234
|
+
end
|
235
|
+
|
236
|
+
progress_action_time action
|
237
|
+
action.poll_attempts[:failed].must_equal 3
|
238
|
+
next_ping(action).must_be_nil
|
239
|
+
action.state.must_equal :error
|
240
|
+
end
|
241
|
+
|
242
|
+
it 'allows increasing poll interval in a time' do
|
243
|
+
TestPollingAction.config.poll_intervals = [1, 2]
|
244
|
+
TestPollingAction.config.attempts_before_next_interval = 1
|
245
|
+
|
246
|
+
action = run_action plan
|
247
|
+
next_ping(action).when.must_equal 1
|
248
|
+
progress_action_time action
|
249
|
+
next_ping(action).when.must_equal 2
|
250
|
+
progress_action_time action
|
251
|
+
next_ping(action).when.must_equal 2
|
252
|
+
end
|
253
|
+
|
254
|
+
end
|
94
255
|
end
|
95
256
|
end
|
data/test/executor_test.rb
CHANGED
@@ -203,8 +203,8 @@ module Dynflow
|
|
203
203
|
result.result.must_equal :success
|
204
204
|
result.state.must_equal :stopped
|
205
205
|
action = world.persistence.load_action result.steps[2]
|
206
|
-
action.output[:progress].must_equal 30
|
207
|
-
action.output[:cancelled].must_equal true
|
206
|
+
action.output[:task][:progress].must_equal 30
|
207
|
+
action.output[:task][:cancelled].must_equal true
|
208
208
|
end
|
209
209
|
end
|
210
210
|
|
@@ -219,7 +219,7 @@ module Dynflow
|
|
219
219
|
step = result.steps[2]
|
220
220
|
step.error.message.must_equal 'action cancelled'
|
221
221
|
action = world.persistence.load_action step
|
222
|
-
action.output[:progress].must_equal 30
|
222
|
+
action.output[:task][:progress].must_equal 30
|
223
223
|
end
|
224
224
|
end
|
225
225
|
|
@@ -241,8 +241,8 @@ module Dynflow
|
|
241
241
|
result.result.must_equal :success
|
242
242
|
result.state.must_equal :stopped
|
243
243
|
action = world.persistence.load_action result.steps[2]
|
244
|
-
action.output[:progress].must_be :<=, 30
|
245
|
-
action.output[:cancelled].must_equal true
|
244
|
+
action.output[:task][:progress].must_be :<=, 30
|
245
|
+
action.output[:task][:cancelled].must_equal true
|
246
246
|
end
|
247
247
|
end
|
248
248
|
end
|
@@ -290,22 +290,14 @@ module Support
|
|
290
290
|
|
291
291
|
def cancel_external_task
|
292
292
|
if input[:text] !~ /cancel-fail/
|
293
|
-
|
293
|
+
external_task.merge(cancelled: true)
|
294
294
|
else
|
295
295
|
error! 'action cancelled'
|
296
296
|
end
|
297
297
|
end
|
298
298
|
|
299
|
-
def external_task=(external_task_data)
|
300
|
-
self.output.update external_task_data
|
301
|
-
end
|
302
|
-
|
303
|
-
def external_task
|
304
|
-
output
|
305
|
-
end
|
306
|
-
|
307
299
|
def done?
|
308
|
-
external_task[:progress] >= 100
|
300
|
+
external_task && external_task[:progress] >= 100
|
309
301
|
end
|
310
302
|
|
311
303
|
def poll_interval
|
@@ -313,7 +305,7 @@ module Support
|
|
313
305
|
end
|
314
306
|
|
315
307
|
def run_progress
|
316
|
-
|
308
|
+
external_task && external_task[:progress].to_f / 100
|
317
309
|
end
|
318
310
|
end
|
319
311
|
|
@@ -325,14 +317,6 @@ module Support
|
|
325
317
|
{ progress: 0, done: false }
|
326
318
|
end
|
327
319
|
|
328
|
-
def external_task=(external_task_data)
|
329
|
-
self.output.update external_task_data
|
330
|
-
end
|
331
|
-
|
332
|
-
def external_task
|
333
|
-
output
|
334
|
-
end
|
335
|
-
|
336
320
|
def poll_external_task
|
337
321
|
if input[:text] == 'troll progress' && !output[:trolled]
|
338
322
|
output[:trolled] = true
|
@@ -340,15 +324,15 @@ module Support
|
|
340
324
|
end
|
341
325
|
|
342
326
|
if input[:text] =~ /pause in progress (\d+)/
|
343
|
-
TestPause.pause if
|
327
|
+
TestPause.pause if external_task[:progress] == $1.to_i
|
344
328
|
end
|
345
329
|
|
346
|
-
progress =
|
330
|
+
progress = external_task[:progress] + 10
|
347
331
|
{ progress: progress, done: progress >= 100 }
|
348
332
|
end
|
349
333
|
|
350
334
|
def done?
|
351
|
-
external_task[:progress] >= 100
|
335
|
+
external_task && external_task[:progress] >= 100
|
352
336
|
end
|
353
337
|
|
354
338
|
def poll_interval
|
@@ -356,7 +340,7 @@ module Support
|
|
356
340
|
end
|
357
341
|
|
358
342
|
def run_progress
|
359
|
-
|
343
|
+
external_task && external_task[:progress].to_f / 100
|
360
344
|
end
|
361
345
|
end
|
362
346
|
|
data/test/test_helper.rb
CHANGED
data/test/testing_test.rb
CHANGED
@@ -45,7 +45,6 @@ module Dynflow
|
|
45
45
|
action.world.must_equal plan.world
|
46
46
|
action.run_step_id.wont_equal action.plan_step_id
|
47
47
|
action.state.must_equal :success
|
48
|
-
action.progress_done.must_equal 1
|
49
48
|
end
|
50
49
|
|
51
50
|
specify '#run_action with suspend' do
|
@@ -53,21 +52,26 @@ module Dynflow
|
|
53
52
|
plan = create_and_plan_action CWE::DummySuspended, input
|
54
53
|
action = run_action plan
|
55
54
|
|
56
|
-
action.output.must_equal 'progress' => 0, 'done' => false
|
57
|
-
action.
|
55
|
+
action.output.must_equal 'task' => { 'progress' => 0, 'done' => false }
|
56
|
+
action.run_progress.must_equal 0
|
58
57
|
|
59
58
|
3.times { progress_action_time action }
|
60
|
-
action.output.must_equal 'progress' => 30, 'done' => false
|
61
|
-
|
59
|
+
action.output.must_equal('task' => { 'progress' => 30, 'done' => false } ,
|
60
|
+
'poll_attempts' => {'total' => 2, 'failed'=> 0 })
|
61
|
+
action.run_progress.must_equal 0.3
|
62
62
|
|
63
63
|
run_action action, Dynflow::Action::Polling::Poll
|
64
64
|
run_action action, Dynflow::Action::Polling::Poll
|
65
|
-
action.output.must_equal 'progress' => 50, 'done' => false
|
66
|
-
|
65
|
+
action.output.must_equal('task' => { 'progress' => 50, 'done' => false },
|
66
|
+
'poll_attempts' => {'total' => 4, 'failed' => 0 })
|
67
|
+
action.run_progress.must_equal 0.5
|
67
68
|
|
68
69
|
5.times { progress_action_time action }
|
69
|
-
|
70
|
-
|
70
|
+
|
71
|
+
|
72
|
+
action.output.must_equal('task' => { 'progress' => 100, 'done' => true },
|
73
|
+
'poll_attempts' => {'total' => 9, 'failed' => 0 })
|
74
|
+
action.run_progress.must_equal 1
|
71
75
|
end
|
72
76
|
|
73
77
|
specify '#finalize_action' do
|
@@ -84,7 +88,6 @@ module Dynflow
|
|
84
88
|
action.world.must_equal plan.world
|
85
89
|
action.finalize_step_id.wont_equal action.run_step_id
|
86
90
|
action.state.must_equal :success
|
87
|
-
action.progress_done.must_equal 1
|
88
91
|
|
89
92
|
$dummy_heavy_progress.must_equal 'dummy_heavy_progress'
|
90
93
|
end
|
data/web/views/flow_step.erb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
<% action = load_action(step) %>
|
2
2
|
|
3
3
|
<% if flow.is_a? Dynflow::Flows::Atom %>
|
4
|
-
<div class="<%= h(atom_css_classes(flow)) %>" style=" width: <%= progress_width(
|
4
|
+
<div class="<%= h(atom_css_classes(flow)) %>" style=" width: <%= progress_width(step) %>%;"></div>
|
5
5
|
<% end %>
|
6
6
|
|
7
7
|
<span class="step-label">
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dynflow
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2014-03-
|
12
|
+
date: 2014-03-25 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|
@@ -286,6 +286,7 @@ files:
|
|
286
286
|
- lib/dynflow/persistence_adapters/abstract.rb
|
287
287
|
- lib/dynflow/persistence_adapters/sequel.rb
|
288
288
|
- lib/dynflow/persistence_adapters/sequel_migrations/001_initial.rb
|
289
|
+
- lib/dynflow/persistence_adapters/sequel_migrations/002_incremental_progress.rb
|
289
290
|
- lib/dynflow/serializable.rb
|
290
291
|
- lib/dynflow/simple_world.rb
|
291
292
|
- lib/dynflow/stateful.rb
|