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 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