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