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 CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- MGY3ZTU0Nzk1ZTJlMjQ3YzRjMTI4ZjkzYTNkODZhYWIzYzZhYzc4ZQ==
4
+ ODk3MTc2Nzc3ZDFmNTMwYTNiMDFjZmIyOWIxZjI5NTY4ZGRjNWE5MQ==
5
5
  data.tar.gz: !binary |-
6
- NTU1MDY2YTBhNjk2Mjc2YjJkNDhkM2JhYzFjYzk1ODU3ZDUzNDc2OQ==
6
+ ZGVhZTBhMGZkNWFkYzcxNWZjY2I2MDNlM2ZiNzFlMmJkNGRhZTQ4NQ==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- ZDRiZmQ2ODQ1MjEyYThmMDg4YjkyNDE1Y2Q5MTI1OWNkMDNjODlmODE5ODlk
10
- N2Q2NDQyNzYyNDQzOWFmZGRhMjZmYTEwYWM1YTkyZTdlMDExMWY0NmU4N2Zl
11
- MzkxYjFlZDgxY2I1MWQ4ZjhhZTgxZDY4NTI3NmRkMTBhNDE5NzE=
9
+ ODBjYjZmNWNmODU5ZWE4Y2JiYTBjNmE4OTYyNWExNjc0ZTg2OGMxZDI5NjEw
10
+ ZDUyNWNiODgxNGE2NTBhYmU0ODdkM2QyNmU3ODdmMDRjZDY4Y2U0NDdjM2Y5
11
+ OWRhMGViNWMyMjk0YmM2N2U2ZjE0ZDFkZmY0NjAwNDg3MDk5M2E=
12
12
  data.tar.gz: !binary |-
13
- MGQyNzY3Y2QwZTZhYTQwNGIwYzY4NzgyNzg3NjVhZTliMDBmZTNjMzBkNDc0
14
- ZTRhMGZhNGJiODEwZjIzODNlMDczMmU0ZDNlNDIwYzQyZmJkMmUzNzhjNDQz
15
- MDRmMzMxNzdmMWZhNWRlOWE4Yjk1Nzc5Njc2MjAyMmUwNzBhMzU=
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(*args)
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(serialized_args)
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)
@@ -283,8 +283,12 @@ module Dynflow
283
283
  end
284
284
 
285
285
  def execute_schedule(schedule_options, *args)
286
- world.middleware.execute(:schedule, self, schedule_options, *args) do
287
- @serializer = schedule(schedule_options, *args)
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
@@ -11,7 +11,7 @@ module Dynflow
11
11
  end
12
12
 
13
13
  def cancel!
14
- NotImplementedError
14
+ raise NotImplementedError
15
15
  end
16
16
  end
17
17
  end
@@ -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, options, schedule_options, *args)
156
- prepare(action_class, options)
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
- update_state :scheduled
159
- entry_action.execute_schedule(schedule_options, args)
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
- :pending,
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 ||= { pending: [:running, :error],
55
- running: [:success, :error],
56
- success: [],
57
- suspended: [],
58
- skipped: [],
59
- error: [] }
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,
@@ -65,6 +65,7 @@ module Dynflow
65
65
 
66
66
  def load_scheduled_plan(execution_plan_id)
67
67
  hash = adapter.load_scheduled_plan(execution_plan_id)
68
+ return nil unless hash
68
69
  ScheduledPlan.new_from_hash(@world, hash)
69
70
  end
70
71
 
@@ -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, :args
6
+ attr_reader :execution_plan_uuid, :start_at, :start_before
7
7
 
8
- def initialize(world, execution_plan_uuid, start_at, start_before, args = [], args_serializer = nil)
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(*@args)
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
- :args => @args_serializer.serialize(*@args),
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
- def serialize(*args)
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(serialized_args)
35
+ def deserialize
10
36
  raise NotImplementedError
11
37
  end
12
38
 
@@ -2,11 +2,11 @@ module Dynflow
2
2
  module Serializers
3
3
  class Noop < Abstract
4
4
 
5
- def serialize(*args)
5
+ def serialize
6
6
  args
7
7
  end
8
8
 
9
- def deserialize(serialized_args)
9
+ def deserialize
10
10
  serialized_args
11
11
  end
12
12
 
@@ -1,3 +1,3 @@
1
1
  module Dynflow
2
- VERSION = '0.8.2'
2
+ VERSION = '0.8.3'
3
3
  end
@@ -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]
@@ -114,7 +114,7 @@ module Dynflow
114
114
  when :success
115
115
  "success"
116
116
  when :error
117
- "important"
117
+ "danger"
118
118
  else
119
119
  "default"
120
120
  end
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, {}, schedule_options, *args)
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
- def plan
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
@@ -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(:plan) do
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) { plan.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
- plan.start_at.inspect.must_equal (@start_at).inspect
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
- plan
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
- plan.plan
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 = plan.execute
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
- @start_at = Time.now.utc + 180
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.deserialize(save_and_load.call(serializer.serialize *input))
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
@@ -80,7 +80,7 @@ module Support
80
80
 
81
81
  def schedule(schedule_options, *args)
82
82
  log 'schedule'
83
- Dynflow::Serializers::Noop.new
83
+ Dynflow::Serializers::Noop.new(args)
84
84
  end
85
85
 
86
86
  def plan(input)
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.disable_executor_auto_termination!
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.2
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-07-31 00:00:00.000000000 Z
12
+ date: 2015-08-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport