dynflow 0.8.2 → 0.8.3
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 +8 -8
- data/examples/future_execution.rb +3 -3
- data/lib/dynflow/action.rb +10 -4
- data/lib/dynflow/action/cancellable.rb +1 -1
- data/lib/dynflow/action/with_sub_plans.rb +12 -2
- data/lib/dynflow/execution_plan.rb +47 -7
- data/lib/dynflow/execution_plan/steps/abstract.rb +1 -1
- data/lib/dynflow/execution_plan/steps/plan_step.rb +15 -7
- data/lib/dynflow/persistence.rb +1 -0
- data/lib/dynflow/persistence_adapters/sequel.rb +3 -0
- data/lib/dynflow/scheduled_plan.rb +11 -7
- data/lib/dynflow/serializers/abstract.rb +28 -2
- data/lib/dynflow/serializers/noop.rb +2 -2
- data/lib/dynflow/version.rb +1 -1
- data/lib/dynflow/web/console.rb +10 -0
- data/lib/dynflow/web/console_helpers.rb +1 -1
- data/lib/dynflow/world.rb +1 -8
- data/test/action_test.rb +26 -3
- data/test/execution_plan_test.rb +22 -0
- data/test/future_execution_test.rb +38 -12
- data/test/support/dummy_example.rb +20 -0
- data/test/support/middleware_example.rb +1 -1
- data/test/test_helper.rb +1 -1
- data/web/views/show.erb +4 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
ODk3MTc2Nzc3ZDFmNTMwYTNiMDFjZmIyOWIxZjI5NTY4ZGRjNWE5MQ==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
ZGVhZTBhMGZkNWFkYzcxNWZjY2I2MDNlM2ZiNzFlMmJkNGRhZTQ4NQ==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
ODBjYjZmNWNmODU5ZWE4Y2JiYTBjNmE4OTYyNWExNjc0ZTg2OGMxZDI5NjEw
|
10
|
+
ZDUyNWNiODgxNGE2NTBhYmU0ODdkM2QyNmU3ODdmMDRjZDY4Y2U0NDdjM2Y5
|
11
|
+
OWRhMGViNWMyMjk0YmM2N2U2ZjE0ZDFkZmY0NjAwNDg3MDk5M2E=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
YmU2NGFlOWU3YWY1NGRhMjc0MDA3Yjg4OTU2OTY1NzAyNjNjZTY1YzUzN2Fl
|
14
|
+
MWIyNDlkYmM0ZDVkMjAxMTRmYjIwNDM3MjE2OTBkMGRmOGRiY2ExODY3YzQ2
|
15
|
+
NDQzNzZhYmRiNWM4ZTUyYmU5YzcwYzk2ZWZjM2JmYmZmYTYyMGY=
|
@@ -12,13 +12,13 @@ class CustomPassedObject
|
|
12
12
|
end
|
13
13
|
|
14
14
|
class CustomPassedObjectSerializer < ::Dynflow::Serializers::Abstract
|
15
|
-
def serialize
|
15
|
+
def serialize
|
16
16
|
object = args.first
|
17
17
|
# Serialized output can be anything that is representable as JSON: Array, Hash...
|
18
18
|
{ :id => object.id, :name => object.name }
|
19
19
|
end
|
20
20
|
|
21
|
-
def deserialize
|
21
|
+
def deserialize
|
22
22
|
# Deserialized output must be an Array
|
23
23
|
[CustomPassedObject.new(serialized_args[:id], serialized_args[:name])]
|
24
24
|
end
|
@@ -27,7 +27,7 @@ end
|
|
27
27
|
class DelayedAction < Dynflow::Action
|
28
28
|
|
29
29
|
def schedule(schedule_options, *args)
|
30
|
-
CustomPassedObjectSerializer.new
|
30
|
+
CustomPassedObjectSerializer.new(args)
|
31
31
|
end
|
32
32
|
|
33
33
|
def plan(passed_object)
|
data/lib/dynflow/action.rb
CHANGED
@@ -283,8 +283,12 @@ module Dynflow
|
|
283
283
|
end
|
284
284
|
|
285
285
|
def execute_schedule(schedule_options, *args)
|
286
|
-
|
287
|
-
|
286
|
+
with_error_handling(true) do
|
287
|
+
world.middleware.execute(:schedule, self, schedule_options, *args) do
|
288
|
+
@serializer = schedule(schedule_options, *args).tap do |serializer|
|
289
|
+
serializer.perform_serialization!
|
290
|
+
end
|
291
|
+
end
|
288
292
|
end
|
289
293
|
end
|
290
294
|
|
@@ -310,7 +314,7 @@ module Dynflow
|
|
310
314
|
end
|
311
315
|
|
312
316
|
def schedule(schedule_options, *args)
|
313
|
-
Serializers::Noop.new
|
317
|
+
Serializers::Noop.new(args)
|
314
318
|
end
|
315
319
|
|
316
320
|
# @override to implement the action's *Plan phase* behaviour.
|
@@ -411,7 +415,7 @@ module Dynflow
|
|
411
415
|
end
|
412
416
|
|
413
417
|
def with_error_handling(propagate_error = nil, &block)
|
414
|
-
raise "wrong state #{self.state}" unless [:skipping, :running].include?(self.state)
|
418
|
+
raise "wrong state #{self.state}" unless [:scheduling, :skipping, :running].include?(self.state)
|
415
419
|
|
416
420
|
begin
|
417
421
|
catch(ERROR) { block.call }
|
@@ -422,6 +426,8 @@ module Dynflow
|
|
422
426
|
end
|
423
427
|
|
424
428
|
case self.state
|
429
|
+
when :scheduling
|
430
|
+
self.state = :pending
|
425
431
|
when :running
|
426
432
|
self.state = :success
|
427
433
|
when :skipping
|
@@ -1,5 +1,7 @@
|
|
1
1
|
module Dynflow
|
2
2
|
module Action::WithSubPlans
|
3
|
+
include Dynflow::Action::Cancellable
|
4
|
+
|
3
5
|
SubPlanFinished = Algebrick.type do
|
4
6
|
fields! :execution_plan_id => String,
|
5
7
|
:success => type { variants TrueClass, FalseClass }
|
@@ -17,6 +19,9 @@ module Dynflow
|
|
17
19
|
(on SubPlanFinished do
|
18
20
|
mark_as_done(event.execution_plan_id, event.success)
|
19
21
|
try_to_finish or suspend
|
22
|
+
end),
|
23
|
+
(on Action::Cancellable::Cancel do
|
24
|
+
cancel!
|
20
25
|
end)
|
21
26
|
end
|
22
27
|
|
@@ -49,6 +54,11 @@ module Dynflow
|
|
49
54
|
def on_finish
|
50
55
|
end
|
51
56
|
|
57
|
+
def cancel!
|
58
|
+
sub_plans('state' => 'running').each(&:cancel)
|
59
|
+
suspend
|
60
|
+
end
|
61
|
+
|
52
62
|
# Helper for creating sub plans
|
53
63
|
def trigger(*args)
|
54
64
|
world.trigger { world.plan_with_caller(self, *args) }
|
@@ -92,9 +102,9 @@ module Dynflow
|
|
92
102
|
end
|
93
103
|
end
|
94
104
|
|
95
|
-
def sub_plans
|
105
|
+
def sub_plans(filter = {})
|
96
106
|
@sub_plans ||= world.persistence.find_execution_plans(filters: { 'caller_execution_plan_id' => execution_plan_id,
|
97
|
-
'caller_action_id' => self.id } )
|
107
|
+
'caller_action_id' => self.id }.merge(filter) )
|
98
108
|
end
|
99
109
|
|
100
110
|
def notify_on_finish(plans)
|
@@ -20,7 +20,7 @@ module Dynflow
|
|
20
20
|
end
|
21
21
|
|
22
22
|
def self.state_transitions
|
23
|
-
@state_transitions ||= { pending: [:scheduled, :planning],
|
23
|
+
@state_transitions ||= { pending: [:stopped, :scheduled, :planning],
|
24
24
|
scheduled: [:planning, :stopped],
|
25
25
|
planning: [:planned, :stopped],
|
26
26
|
planned: [:running],
|
@@ -152,11 +152,19 @@ module Dynflow
|
|
152
152
|
@last_step_id += 1
|
153
153
|
end
|
154
154
|
|
155
|
-
def schedule(action_class,
|
156
|
-
|
155
|
+
def schedule(action_class, schedule_options, *args)
|
156
|
+
save
|
157
|
+
@root_plan_step = add_scheduling_step(action_class)
|
157
158
|
execution_history.add("schedule", @world.id)
|
158
|
-
|
159
|
-
|
159
|
+
serializer = root_plan_step.schedule(schedule_options, args)
|
160
|
+
scheduled_plan = ScheduledPlan.new(@world,
|
161
|
+
id,
|
162
|
+
schedule_options[:start_at],
|
163
|
+
schedule_options.fetch(:start_before, nil),
|
164
|
+
serializer)
|
165
|
+
persistence.save_scheduled_plan(scheduled_plan)
|
166
|
+
ensure
|
167
|
+
update_state(error? ? :stopped : :scheduled)
|
160
168
|
end
|
161
169
|
|
162
170
|
def schedule_record
|
@@ -195,6 +203,31 @@ module Dynflow
|
|
195
203
|
update_state(error? ? :stopped : :planned)
|
196
204
|
end
|
197
205
|
|
206
|
+
# sends the cancel event to all currently running and cancellable steps.
|
207
|
+
# if the plan is just scheduled, it cancels it (and returns an one-item
|
208
|
+
# array with the future value of the cancel result)
|
209
|
+
def cancel
|
210
|
+
if state == :scheduled
|
211
|
+
[Concurrent.future.tap { |f| f.success schedule_record.cancel }]
|
212
|
+
else
|
213
|
+
steps_to_cancel.map do |step|
|
214
|
+
world.event(id, step.id, ::Dynflow::Action::Cancellable::Cancel)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def cancellable?
|
220
|
+
return true if state == :scheduled
|
221
|
+
return false unless state == :running
|
222
|
+
steps_to_cancel.any?
|
223
|
+
end
|
224
|
+
|
225
|
+
def steps_to_cancel
|
226
|
+
steps_in_state(:running, :suspended).find_all do |step|
|
227
|
+
step.action(self).is_a?(::Dynflow::Action::Cancellable)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
198
231
|
def skip(step)
|
199
232
|
steps_to_skip = steps_to_skip(step).each(&:mark_to_skip)
|
200
233
|
self.save
|
@@ -251,6 +284,12 @@ module Dynflow
|
|
251
284
|
current_run_flow.add_and_resolve(@dependency_graph, new_flow) if current_run_flow
|
252
285
|
end
|
253
286
|
|
287
|
+
def add_scheduling_step(action_class)
|
288
|
+
add_step(Steps::PlanStep, action_class, generate_action_id, :scheduling).tap do |step|
|
289
|
+
step.initialize_action
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
254
293
|
def add_plan_step(action_class, caller_action = nil)
|
255
294
|
add_step(Steps::PlanStep, action_class, generate_action_id).tap do |step|
|
256
295
|
# TODO: to be removed and preferred by the caller_action
|
@@ -323,6 +362,7 @@ module Dynflow
|
|
323
362
|
# @return [0..1] the percentage of the progress. See Action::Progress for more
|
324
363
|
# info
|
325
364
|
def progress
|
365
|
+
return 0 if [:pending, :planning, :scheduled].include?(state)
|
326
366
|
flow_step_ids = run_flow.all_step_ids + finalize_flow.all_step_ids
|
327
367
|
plan_done, plan_total = flow_step_ids.reduce([0.0, 0]) do |(done, total), step_id|
|
328
368
|
step = self.steps[step_id]
|
@@ -353,10 +393,10 @@ module Dynflow
|
|
353
393
|
world.persistence
|
354
394
|
end
|
355
395
|
|
356
|
-
def add_step(step_class, action_class, action_id)
|
396
|
+
def add_step(step_class, action_class, action_id, state = :pending)
|
357
397
|
step_class.new(self.id,
|
358
398
|
self.generate_step_id,
|
359
|
-
|
399
|
+
state,
|
360
400
|
action_class,
|
361
401
|
action_id,
|
362
402
|
nil,
|
@@ -63,7 +63,7 @@ module Dynflow
|
|
63
63
|
end
|
64
64
|
|
65
65
|
def self.states
|
66
|
-
@states ||= [:pending, :running, :success, :suspended, :skipping, :skipped, :error]
|
66
|
+
@states ||= [:scheduling, :pending, :running, :success, :suspended, :skipping, :skipped, :error]
|
67
67
|
end
|
68
68
|
|
69
69
|
def execute(*args)
|
@@ -35,6 +35,13 @@ module Dynflow
|
|
35
35
|
super.merge recursive_to_hash(:children => children)
|
36
36
|
end
|
37
37
|
|
38
|
+
def schedule(schedule_options, args)
|
39
|
+
@action.execute_schedule(schedule_options, *args)
|
40
|
+
@action.serializer
|
41
|
+
ensure
|
42
|
+
save
|
43
|
+
end
|
44
|
+
|
38
45
|
# @return [Action]
|
39
46
|
def execute(execution_plan, trigger, from_subscription, *args)
|
40
47
|
unless @action
|
@@ -51,12 +58,13 @@ module Dynflow
|
|
51
58
|
end
|
52
59
|
|
53
60
|
def self.state_transitions
|
54
|
-
@state_transitions ||= {
|
55
|
-
|
56
|
-
success:
|
57
|
-
|
58
|
-
|
59
|
-
|
61
|
+
@state_transitions ||= { scheduling: [:pending, :error],
|
62
|
+
pending: [:running, :error],
|
63
|
+
running: [:success, :error],
|
64
|
+
success: [],
|
65
|
+
suspended: [],
|
66
|
+
skipped: [],
|
67
|
+
error: [] }
|
60
68
|
end
|
61
69
|
|
62
70
|
|
@@ -80,7 +88,7 @@ module Dynflow
|
|
80
88
|
@action = @world.persistence.load_action(self)
|
81
89
|
end
|
82
90
|
|
83
|
-
def initialize_action(caller_action)
|
91
|
+
def initialize_action(caller_action = nil)
|
84
92
|
attributes = { execution_plan_id: execution_plan_id,
|
85
93
|
id: action_id,
|
86
94
|
step: self,
|
data/lib/dynflow/persistence.rb
CHANGED
@@ -63,6 +63,7 @@ module Dynflow
|
|
63
63
|
filter(:execution_plan, table(:execution_plan), filters).each_slice(batch_size) do |plans|
|
64
64
|
uuids = plans.map { |p| p.fetch(:uuid) }
|
65
65
|
@db.transaction do
|
66
|
+
table(:scheduled).where(execution_plan_uuid: uuids).delete
|
66
67
|
table(:step).where(execution_plan_uuid: uuids).delete
|
67
68
|
table(:action).where(execution_plan_uuid: uuids).delete
|
68
69
|
count += table(:execution_plan).where(uuid: uuids).delete
|
@@ -100,6 +101,8 @@ module Dynflow
|
|
100
101
|
|
101
102
|
def load_scheduled_plan(execution_plan_id)
|
102
103
|
load :scheduled, execution_plan_uuid: execution_plan_id
|
104
|
+
rescue KeyError
|
105
|
+
return nil
|
103
106
|
end
|
104
107
|
|
105
108
|
def save_scheduled_plan(execution_plan_id, value)
|
@@ -3,14 +3,13 @@ module Dynflow
|
|
3
3
|
|
4
4
|
include Algebrick::TypeCheck
|
5
5
|
|
6
|
-
attr_reader :execution_plan_uuid, :start_at, :start_before
|
6
|
+
attr_reader :execution_plan_uuid, :start_at, :start_before
|
7
7
|
|
8
|
-
def initialize(world, execution_plan_uuid, start_at, start_before,
|
8
|
+
def initialize(world, execution_plan_uuid, start_at, start_before, args_serializer)
|
9
9
|
@world = Type! world, World
|
10
10
|
@execution_plan_uuid = Type! execution_plan_uuid, String
|
11
11
|
@start_at = Type! start_at, Time
|
12
12
|
@start_before = Type! start_before, Time, NilClass
|
13
|
-
@args = Type! args, Array
|
14
13
|
@args_serializer = Type! args_serializer, Serializers::Abstract
|
15
14
|
end
|
16
15
|
|
@@ -22,7 +21,7 @@ module Dynflow
|
|
22
21
|
execution_plan.root_plan_step.load_action
|
23
22
|
execution_plan.generate_action_id
|
24
23
|
execution_plan.generate_step_id
|
25
|
-
execution_plan.plan(*@
|
24
|
+
execution_plan.plan(*@args_serializer.perform_deserialization!)
|
26
25
|
end
|
27
26
|
|
28
27
|
def timeout
|
@@ -37,6 +36,12 @@ module Dynflow
|
|
37
36
|
execution_plan.update_state :stopped
|
38
37
|
end
|
39
38
|
|
39
|
+
def cancel
|
40
|
+
error("Scheduled task cancelled", "Scheduled task cancelled")
|
41
|
+
@world.persistence.delete_scheduled_plans(:execution_plan_uuid => execution_plan.id)
|
42
|
+
return true
|
43
|
+
end
|
44
|
+
|
40
45
|
def execute
|
41
46
|
@world.execute(execution_plan.id)
|
42
47
|
end
|
@@ -45,18 +50,17 @@ module Dynflow
|
|
45
50
|
recursive_to_hash :execution_plan_uuid => @execution_plan_uuid,
|
46
51
|
:start_at => time_to_str(@start_at),
|
47
52
|
:start_before => time_to_str(@start_before),
|
48
|
-
:
|
53
|
+
:serialized_args => @args_serializer.serialized_args,
|
49
54
|
:args_serializer => @args_serializer.class.name
|
50
55
|
end
|
51
56
|
|
52
57
|
# @api private
|
53
58
|
def self.new_from_hash(world, hash, *args)
|
54
|
-
serializer = hash[:args_serializer].constantize.new
|
59
|
+
serializer = hash[:args_serializer].constantize.new(nil, hash[:serialized_args])
|
55
60
|
self.new(world,
|
56
61
|
hash[:execution_plan_uuid],
|
57
62
|
string_to_time(hash[:start_at]),
|
58
63
|
string_to_time(hash[:start_before]),
|
59
|
-
serializer.deserialize(hash[:args]),
|
60
64
|
serializer)
|
61
65
|
rescue NameError => e
|
62
66
|
error(e.message)
|
@@ -2,11 +2,37 @@ module Dynflow
|
|
2
2
|
module Serializers
|
3
3
|
class Abstract
|
4
4
|
|
5
|
-
|
5
|
+
attr_reader :args, :serialized_args
|
6
|
+
|
7
|
+
def initialize(args, serialized_args = nil)
|
8
|
+
@args = args
|
9
|
+
@serialized_args = serialized_args
|
10
|
+
end
|
11
|
+
|
12
|
+
def args
|
13
|
+
raise "@args not set" if @args.nil?
|
14
|
+
return @args
|
15
|
+
end
|
16
|
+
|
17
|
+
def serialized_args
|
18
|
+
raise "@serialized_args not set" if @serialized_args.nil?
|
19
|
+
return @serialized_args
|
20
|
+
end
|
21
|
+
|
22
|
+
def perform_serialization!
|
23
|
+
@serialized_args = serialize
|
24
|
+
end
|
25
|
+
|
26
|
+
def perform_deserialization!
|
27
|
+
raise "@serialized_args not set" if @serialized_args.nil?
|
28
|
+
@args = deserialize
|
29
|
+
end
|
30
|
+
|
31
|
+
def serialize
|
6
32
|
raise NotImplementedError
|
7
33
|
end
|
8
34
|
|
9
|
-
def deserialize
|
35
|
+
def deserialize
|
10
36
|
raise NotImplementedError
|
11
37
|
end
|
12
38
|
|
data/lib/dynflow/version.rb
CHANGED
data/lib/dynflow/web/console.rb
CHANGED
@@ -64,6 +64,16 @@ module Dynflow
|
|
64
64
|
end
|
65
65
|
end
|
66
66
|
|
67
|
+
post('/:id/cancel') do |id|
|
68
|
+
plan = world.persistence.load_execution_plan(id)
|
69
|
+
cancel_events = plan.cancel
|
70
|
+
if cancel_events.empty?
|
71
|
+
redirect(url "/#{plan.id}?notice=#{url_encode('Not possible to cancel at the moment')}")
|
72
|
+
else
|
73
|
+
redirect(url "/#{plan.id}?notice=#{url_encode("The cancel event has been propagated")}")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
67
77
|
post('/:id/skip/:step_id') do |id, step_id|
|
68
78
|
plan = world.persistence.load_execution_plan(id)
|
69
79
|
step = plan.steps[step_id.to_i]
|
data/lib/dynflow/world.rb
CHANGED
@@ -149,14 +149,7 @@ module Dynflow
|
|
149
149
|
def schedule(action_class, schedule_options, *args)
|
150
150
|
raise 'No action_class given' if action_class.nil?
|
151
151
|
execution_plan = ExecutionPlan.new self
|
152
|
-
execution_plan.schedule(action_class,
|
153
|
-
scheduled_plan = ScheduledPlan.new(self,
|
154
|
-
execution_plan.id,
|
155
|
-
schedule_options[:start_at],
|
156
|
-
schedule_options.fetch(:start_before, nil),
|
157
|
-
args,
|
158
|
-
execution_plan.entry_action.serializer)
|
159
|
-
persistence.save_scheduled_plan(scheduled_plan)
|
152
|
+
execution_plan.schedule(action_class, schedule_options, *args)
|
160
153
|
Scheduled[execution_plan.id]
|
161
154
|
end
|
162
155
|
|
data/test/action_test.rb
CHANGED
@@ -348,7 +348,7 @@ module Dynflow
|
|
348
348
|
include Dynflow::Action::WithSubPlans
|
349
349
|
|
350
350
|
def create_sub_plans
|
351
|
-
input[:count].times.map{ trigger(ChildAction) }
|
351
|
+
input[:count].times.map { trigger(ChildAction, suspend: input[:suspend]) }
|
352
352
|
end
|
353
353
|
|
354
354
|
def resume
|
@@ -358,17 +358,22 @@ module Dynflow
|
|
358
358
|
end
|
359
359
|
|
360
360
|
class ChildAction < Dynflow::Action
|
361
|
-
|
361
|
+
include Dynflow::Action::Cancellable
|
362
|
+
|
363
|
+
def plan(input)
|
362
364
|
if FailureSimulator.fail_in_child_plan
|
363
365
|
raise "Fail in child plan"
|
364
366
|
end
|
365
367
|
super
|
366
368
|
end
|
367
369
|
|
368
|
-
def run
|
370
|
+
def run(event = nil)
|
369
371
|
if FailureSimulator.fail_in_child_run
|
370
372
|
raise "Fail in child run"
|
371
373
|
end
|
374
|
+
if input[:suspend] && !(event == Dynflow::Action::Cancellable::Cancel)
|
375
|
+
suspend
|
376
|
+
end
|
372
377
|
end
|
373
378
|
end
|
374
379
|
|
@@ -436,6 +441,24 @@ module Dynflow
|
|
436
441
|
resumed_plan.result.must_equal :success
|
437
442
|
end
|
438
443
|
end
|
444
|
+
|
445
|
+
describe 'cancelling' do
|
446
|
+
include TestHelpers
|
447
|
+
|
448
|
+
it "sends the cancel event to all actions that are running and support cancelling" do
|
449
|
+
triggered_plan = world.trigger(ParentAction, count: 2, suspend: true)
|
450
|
+
plan = wait_for do
|
451
|
+
plan = world.persistence.load_execution_plan(triggered_plan.id)
|
452
|
+
if plan.cancellable?
|
453
|
+
plan
|
454
|
+
end
|
455
|
+
end
|
456
|
+
plan.cancel
|
457
|
+
triggered_plan.finished.wait
|
458
|
+
triggered_plan.finished.value.state.must_equal :stopped
|
459
|
+
triggered_plan.finished.value.result.must_equal :success
|
460
|
+
end
|
461
|
+
end
|
439
462
|
end
|
440
463
|
end
|
441
464
|
end
|
data/test/execution_plan_test.rb
CHANGED
@@ -245,6 +245,28 @@ module Dynflow
|
|
245
245
|
end
|
246
246
|
end
|
247
247
|
|
248
|
+
describe '#cancel' do
|
249
|
+
include TestHelpers
|
250
|
+
|
251
|
+
let :execution_plan do
|
252
|
+
world.plan(Support::CodeWorkflowExample::CancelableSuspended, { text: 'cancel-external' })
|
253
|
+
end
|
254
|
+
|
255
|
+
it 'cancels' do
|
256
|
+
finished = world.execute(execution_plan.id)
|
257
|
+
plan = wait_for do
|
258
|
+
plan = world.persistence.load_execution_plan(execution_plan.id)
|
259
|
+
if plan.cancellable?
|
260
|
+
plan
|
261
|
+
end
|
262
|
+
end
|
263
|
+
cancel_events = plan.cancel
|
264
|
+
cancel_events.size.must_equal 1
|
265
|
+
cancel_events.each(&:wait)
|
266
|
+
finished.wait
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
248
270
|
describe 'accessing actions results' do
|
249
271
|
let :execution_plan do
|
250
272
|
world.plan(Support::CodeWorkflowExample::IncomingIssues, issues_data)
|
@@ -10,11 +10,12 @@ module Dynflow
|
|
10
10
|
describe 'action scheduling' do
|
11
11
|
|
12
12
|
before do
|
13
|
+
@start_at = Time.now.utc + 180
|
13
14
|
world.persistence.delete_scheduled_plans(:execution_plan_uuid => [])
|
14
15
|
end
|
15
16
|
|
16
17
|
let(:world) { WorldFactory.create_world }
|
17
|
-
let(:
|
18
|
+
let(:scheduled_plan) do
|
18
19
|
scheduled = world.schedule(::Support::DummyExample::Dummy, { :start_at => @start_at })
|
19
20
|
scheduled.must_be :scheduled?
|
20
21
|
world.persistence.load_scheduled_plan(scheduled.execution_plan_id)
|
@@ -22,32 +23,53 @@ module Dynflow
|
|
22
23
|
let(:history_names) do
|
23
24
|
->(execution_plan) { execution_plan.execution_history.map(&:name) }
|
24
25
|
end
|
25
|
-
let(:execution_plan) {
|
26
|
+
let(:execution_plan) { scheduled_plan.execution_plan }
|
27
|
+
|
28
|
+
it 'returns the progress as 0' do
|
29
|
+
execution_plan.progress.must_equal 0
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'marks the plan as failed when issues in serialied phase' do
|
33
|
+
world.persistence.delete_execution_plans({})
|
34
|
+
e = proc { world.schedule(::Support::DummyExample::DummyCustomScheduleSerializer, { :start_at => @start_at }, :fail) }.must_raise RuntimeError
|
35
|
+
e.message.must_equal 'Enforced serializer failure'
|
36
|
+
plan = world.persistence.find_execution_plans(page: 0, per_page: 1, order_by: :ended_at, desc: true).first
|
37
|
+
plan.state.must_equal :stopped
|
38
|
+
plan.result.must_equal :error
|
39
|
+
end
|
26
40
|
|
27
41
|
it 'schedules the action' do
|
28
|
-
@start_at = Time.now.utc + 180
|
29
42
|
execution_plan.steps.count.must_equal 1
|
30
|
-
|
43
|
+
scheduled_plan.start_at.inspect.must_equal (@start_at).inspect
|
31
44
|
history_names.call(execution_plan).must_equal ['schedule']
|
32
45
|
end
|
33
46
|
|
47
|
+
it 'allows cancelling the scheduled plan' do
|
48
|
+
execution_plan.state.must_equal :scheduled
|
49
|
+
execution_plan.cancellable?.must_equal true
|
50
|
+
execution_plan.cancel.each(&:wait)
|
51
|
+
execution_plan = world.persistence.load_execution_plan(self.execution_plan.id)
|
52
|
+
execution_plan.state.must_equal :stopped
|
53
|
+
execution_plan.result.must_equal :error
|
54
|
+
execution_plan.schedule_record.must_equal nil
|
55
|
+
end
|
56
|
+
|
34
57
|
it 'finds scheduled plans' do
|
35
58
|
@start_at = Time.now.utc - 100
|
36
|
-
|
59
|
+
scheduled_plan
|
37
60
|
past_scheduled_plans = world.persistence.find_past_scheduled_plans(@start_at + 10)
|
38
61
|
past_scheduled_plans.length.must_equal 1
|
39
62
|
past_scheduled_plans.first.execution_plan_uuid.must_equal execution_plan.id
|
40
63
|
end
|
41
64
|
|
42
65
|
it 'scheduled plans can be planned and executed' do
|
43
|
-
@start_at = Time.now.utc + 180
|
44
66
|
execution_plan.state.must_equal :scheduled
|
45
|
-
|
67
|
+
scheduled_plan.plan
|
46
68
|
execution_plan.state.must_equal :planned
|
47
69
|
execution_plan.result.must_equal :pending
|
48
70
|
assert_planning_success execution_plan
|
49
71
|
history_names.call(execution_plan).must_equal ['schedule']
|
50
|
-
executed =
|
72
|
+
executed = scheduled_plan.execute
|
51
73
|
executed.wait
|
52
74
|
executed.value.state.must_equal :stopped
|
53
75
|
executed.value.result.must_equal :success
|
@@ -55,8 +77,7 @@ module Dynflow
|
|
55
77
|
end
|
56
78
|
|
57
79
|
it 'expired plans can be failed' do
|
58
|
-
|
59
|
-
plan.timeout
|
80
|
+
scheduled_plan.timeout
|
60
81
|
execution_plan.state.must_equal :stopped
|
61
82
|
execution_plan.result.must_equal :error
|
62
83
|
execution_plan.errors.first.message.must_match /could not be started before set time/
|
@@ -97,10 +118,15 @@ module Dynflow
|
|
97
118
|
let(:save_and_load) do
|
98
119
|
->(thing) { MultiJson.load(MultiJson.dump(thing)) }
|
99
120
|
end
|
121
|
+
|
100
122
|
let(:simulated_use) do
|
101
123
|
lambda do |serializer_class, input|
|
102
|
-
serializer = serializer_class.new
|
103
|
-
serializer.
|
124
|
+
serializer = serializer_class.new(input)
|
125
|
+
serializer.perform_serialization!
|
126
|
+
serialized_args = save_and_load.call(serializer.serialized_args)
|
127
|
+
serializer = serializer_class.new(nil, serialized_args)
|
128
|
+
serializer.perform_deserialization!
|
129
|
+
serializer.args
|
104
130
|
end
|
105
131
|
end
|
106
132
|
|
@@ -6,6 +6,26 @@ module Support
|
|
6
6
|
def run; end
|
7
7
|
end
|
8
8
|
|
9
|
+
class MySerializer < Dynflow::Serializers::Abstract
|
10
|
+
def serialize
|
11
|
+
if args.first == :fail
|
12
|
+
raise 'Enforced serializer failure'
|
13
|
+
end
|
14
|
+
args
|
15
|
+
end
|
16
|
+
|
17
|
+
def deserialize
|
18
|
+
serialized_args
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class DummyCustomScheduleSerializer < Dynflow::Action
|
23
|
+
def schedule(schedule_options, *args)
|
24
|
+
MySerializer.new(args)
|
25
|
+
end
|
26
|
+
def run; end
|
27
|
+
end
|
28
|
+
|
9
29
|
class FailingDummy < Dynflow::Action
|
10
30
|
def run; raise 'error'; end
|
11
31
|
end
|
data/test/test_helper.rb
CHANGED
@@ -20,7 +20,7 @@ require 'support/rescue_example'
|
|
20
20
|
require 'support/dummy_example'
|
21
21
|
require 'support/test_execution_log'
|
22
22
|
|
23
|
-
Concurrent.
|
23
|
+
Concurrent.disable_at_exit_handlers!
|
24
24
|
|
25
25
|
# To be able to stop a process in some step and perform assertions while paused
|
26
26
|
class TestPause
|
data/web/views/show.erb
CHANGED
@@ -4,6 +4,9 @@
|
|
4
4
|
<% if @plan.state == :paused %>
|
5
5
|
<a href="<%= url("/#{@plan.id}/resume") %>" class="postlink">Resume</a>
|
6
6
|
<% end %>
|
7
|
+
<% if @plan.cancellable? %>
|
8
|
+
<a href="<%= url("/#{@plan.id}/cancel") %>" class="postlink">Cancel</a>
|
9
|
+
<% end %>
|
7
10
|
</p>
|
8
11
|
|
9
12
|
<p>
|
@@ -11,7 +14,7 @@
|
|
11
14
|
<%= h(@plan.result) %>
|
12
15
|
</p>
|
13
16
|
|
14
|
-
<% if @plan.state == :scheduled %>
|
17
|
+
<% if @plan.state == :scheduled && @plan.schedule_record %>
|
15
18
|
<p>
|
16
19
|
<b>Start at:</b>
|
17
20
|
<%= h(@plan.schedule_record.start_at) %>
|
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.8.
|
4
|
+
version: 0.8.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ivan Necas
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-
|
12
|
+
date: 2015-08-13 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|