dynflow 0.5.1 → 0.6.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.
- 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
|