dynflow 0.8.1 → 0.8.2

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.
Files changed (38) hide show
  1. checksums.yaml +8 -8
  2. data/doc/pages/source/documentation/index.md +47 -12
  3. data/dynflow.gemspec +2 -2
  4. data/examples/future_execution.rb +73 -0
  5. data/lib/dynflow.rb +4 -1
  6. data/lib/dynflow/action.rb +15 -0
  7. data/lib/dynflow/config.rb +15 -1
  8. data/lib/dynflow/coordinator.rb +7 -0
  9. data/lib/dynflow/execution_plan.rb +15 -3
  10. data/lib/dynflow/execution_plan/steps/plan_step.rb +5 -1
  11. data/lib/dynflow/middleware.rb +4 -0
  12. data/lib/dynflow/middleware/stack.rb +1 -1
  13. data/lib/dynflow/middleware/world.rb +1 -1
  14. data/lib/dynflow/persistence.rb +19 -0
  15. data/lib/dynflow/persistence_adapters/abstract.rb +16 -0
  16. data/lib/dynflow/persistence_adapters/sequel.rb +31 -3
  17. data/lib/dynflow/persistence_adapters/sequel_migrations/006_fix_data_length.rb +17 -0
  18. data/lib/dynflow/persistence_adapters/sequel_migrations/007_future_execution.rb +13 -0
  19. data/lib/dynflow/scheduled_plan.rb +65 -0
  20. data/lib/dynflow/schedulers.rb +9 -0
  21. data/lib/dynflow/schedulers/abstract.rb +37 -0
  22. data/lib/dynflow/schedulers/abstract_core.rb +65 -0
  23. data/lib/dynflow/schedulers/polling.rb +32 -0
  24. data/lib/dynflow/serializers.rb +8 -0
  25. data/lib/dynflow/serializers/abstract.rb +15 -0
  26. data/lib/dynflow/serializers/noop.rb +15 -0
  27. data/lib/dynflow/version.rb +1 -1
  28. data/lib/dynflow/web/console.rb +8 -23
  29. data/lib/dynflow/web/console_helpers.rb +10 -0
  30. data/lib/dynflow/world.rb +99 -24
  31. data/test/abnormal_states_recovery_test.rb +64 -0
  32. data/test/future_execution_test.rb +114 -0
  33. data/test/middleware_test.rb +8 -2
  34. data/test/support/middleware_example.rb +11 -0
  35. data/test/test_helper.rb +1 -0
  36. data/web/views/show.erb +11 -0
  37. data/web/views/worlds.erb +19 -3
  38. metadata +19 -6
@@ -55,6 +55,22 @@ module Dynflow
55
55
  raise NotImplementedError
56
56
  end
57
57
 
58
+ def find_past_scheduled_plans(options = {})
59
+ raise NotImplementedError
60
+ end
61
+
62
+ def delete_scheduled_plans(filters, batch_size = 1000)
63
+ raise NotImplementedError
64
+ end
65
+
66
+ def load_scheduled_plan(execution_plan_id)
67
+ raise NotImplementedError
68
+ end
69
+
70
+ def save_scheduled_plan(execution_plan_id, value)
71
+ raise NotImplementedError
72
+ end
73
+
58
74
  def load_step(execution_plan_id, step_id)
59
75
  raise NotImplementedError
60
76
  end
@@ -31,7 +31,8 @@ module Dynflow
31
31
  action: %w(caller_execution_plan_id caller_action_id),
32
32
  step: %w(state started_at ended_at real_time execution_time action_id progress_done progress_weight),
33
33
  envelope: %w(receiver_id),
34
- coordinator_record: %w(id owner_id class) }
34
+ coordinator_record: %w(id owner_id class),
35
+ scheduled: %w(execution_plan_uuid start_at start_before args_serializer)}
35
36
 
36
37
  def initialize(config)
37
38
  config = config.dup
@@ -54,7 +55,6 @@ module Dynflow
54
55
  paginate(table(:execution_plan), options),
55
56
  options),
56
57
  options[:filters])
57
-
58
58
  data_set.map { |record| load_data(record) }
59
59
  end
60
60
 
@@ -79,6 +79,33 @@ module Dynflow
79
79
  save :execution_plan, { uuid: execution_plan_id }, value
80
80
  end
81
81
 
82
+ def delete_scheduled_plans(filters, batch_size = 1000)
83
+ count = 0
84
+ filter(:scheduled, table(:scheduled), filters).each_slice(batch_size) do |plans|
85
+ uuids = plans.map { |p| p.fetch(:execution_plan_uuid) }
86
+ @db.transaction do
87
+ count += table(:scheduled).where(execution_plan_uuid: uuids).delete
88
+ end
89
+ end
90
+ count
91
+ end
92
+
93
+ def find_past_scheduled_plans(time)
94
+ table(:scheduled)
95
+ .where('start_at <= ?', time)
96
+ .order_by(:start_at)
97
+ .all
98
+ .map { |plan| load_data(plan) }
99
+ end
100
+
101
+ def load_scheduled_plan(execution_plan_id)
102
+ load :scheduled, execution_plan_uuid: execution_plan_id
103
+ end
104
+
105
+ def save_scheduled_plan(execution_plan_id, value)
106
+ save :scheduled, { execution_plan_uuid: execution_plan_id }, value
107
+ end
108
+
82
109
  def load_step(execution_plan_id, step_id)
83
110
  load :step, execution_plan_uuid: execution_plan_id, id: step_id
84
111
  end
@@ -164,7 +191,8 @@ module Dynflow
164
191
  action: :dynflow_actions,
165
192
  step: :dynflow_steps,
166
193
  envelope: :dynflow_envelopes,
167
- coordinator_record: :dynflow_coordinator_records }
194
+ coordinator_record: :dynflow_coordinator_records,
195
+ scheduled: :dynflow_scheduled_plans }
168
196
 
169
197
  def table(which)
170
198
  db[TABLES.fetch(which)]
@@ -0,0 +1,17 @@
1
+ Sequel.migration do
2
+ up do
3
+ alter_table(:dynflow_steps) do
4
+ if @db.database_type == :mysql
5
+ set_column_type :data, :mediumtext
6
+ end
7
+ end
8
+ end
9
+
10
+ down do
11
+ alter_table(:dynflow_steps) do
12
+ if @db.database_type == :mysql
13
+ set_column_type :data, :text
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,13 @@
1
+ Sequel.migration do
2
+ change do
3
+ create_table(:dynflow_scheduled_plans) do
4
+ foreign_key :execution_plan_uuid, :dynflow_execution_plans, type: String, size: 36, fixed: true
5
+ index :execution_plan_uuid
6
+ column :start_at, Time
7
+ index :start_at
8
+ column :start_before, Time
9
+ column :data, String, text: true
10
+ column :args_serializer, String
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,65 @@
1
+ module Dynflow
2
+ class ScheduledPlan < Serializable
3
+
4
+ include Algebrick::TypeCheck
5
+
6
+ attr_reader :execution_plan_uuid, :start_at, :start_before, :args
7
+
8
+ def initialize(world, execution_plan_uuid, start_at, start_before, args = [], args_serializer = nil)
9
+ @world = Type! world, World
10
+ @execution_plan_uuid = Type! execution_plan_uuid, String
11
+ @start_at = Type! start_at, Time
12
+ @start_before = Type! start_before, Time, NilClass
13
+ @args = Type! args, Array
14
+ @args_serializer = Type! args_serializer, Serializers::Abstract
15
+ end
16
+
17
+ def execution_plan
18
+ @execution_plan ||= @world.persistence.load_execution_plan(@execution_plan_uuid)
19
+ end
20
+
21
+ def plan
22
+ execution_plan.root_plan_step.load_action
23
+ execution_plan.generate_action_id
24
+ execution_plan.generate_step_id
25
+ execution_plan.plan(*@args)
26
+ end
27
+
28
+ def timeout
29
+ error("Execution plan could not be started before set time (#{@start_before})", 'timeout')
30
+ end
31
+
32
+ def error(message, history_entry = nil)
33
+ execution_plan.root_plan_step.state = :error
34
+ execution_plan.root_plan_step.error = ::Dynflow::ExecutionPlan::Steps::Error.new(message)
35
+ execution_plan.root_plan_step.save
36
+ execution_plan.execution_history.add history_entry, @world.id unless history_entry.nil?
37
+ execution_plan.update_state :stopped
38
+ end
39
+
40
+ def execute
41
+ @world.execute(execution_plan.id)
42
+ end
43
+
44
+ def to_hash
45
+ recursive_to_hash :execution_plan_uuid => @execution_plan_uuid,
46
+ :start_at => time_to_str(@start_at),
47
+ :start_before => time_to_str(@start_before),
48
+ :args => @args_serializer.serialize(*@args),
49
+ :args_serializer => @args_serializer.class.name
50
+ end
51
+
52
+ # @api private
53
+ def self.new_from_hash(world, hash, *args)
54
+ serializer = hash[:args_serializer].constantize.new
55
+ self.new(world,
56
+ hash[:execution_plan_uuid],
57
+ string_to_time(hash[:start_at]),
58
+ string_to_time(hash[:start_before]),
59
+ serializer.deserialize(hash[:args]),
60
+ serializer)
61
+ rescue NameError => e
62
+ error(e.message)
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,9 @@
1
+ module Dynflow
2
+ module Schedulers
3
+
4
+ require 'dynflow/schedulers/abstract'
5
+ require 'dynflow/schedulers/abstract_core'
6
+ require 'dynflow/schedulers/polling'
7
+
8
+ end
9
+ end
@@ -0,0 +1,37 @@
1
+ module Dynflow
2
+ module Schedulers
3
+ class Abstract
4
+
5
+ attr_reader :core
6
+
7
+ def initialize(world, options = {})
8
+ @world = world
9
+ @options = options
10
+ spawn
11
+ end
12
+
13
+ def start
14
+ @core.ask(:start)
15
+ end
16
+
17
+ def terminate
18
+ @core.ask(:terminate!)
19
+ end
20
+
21
+ private
22
+
23
+ def core_class
24
+ raise NotImplementedError
25
+ end
26
+
27
+ def spawn
28
+ Concurrent.future.tap do |initialized|
29
+ @core = core_class.spawn name: 'scheduler',
30
+ args: [@world, @options],
31
+ initialized: initialized
32
+ end
33
+ end
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,65 @@
1
+ module Dynflow
2
+ module Schedulers
3
+ class AbstractCore < Actor
4
+
5
+ include Algebrick::TypeCheck
6
+ attr_reader :world, :logger
7
+
8
+ def initialize(world, options = {})
9
+ @world = Type! world, World
10
+ @logger = world.logger
11
+ configure(options)
12
+ end
13
+
14
+ def start
15
+ raise NotImplementedError
16
+ end
17
+
18
+ def configure(options)
19
+ @time_source = options.fetch(:time_source, -> { Time.now.utc })
20
+ end
21
+
22
+ def check_schedule
23
+ raise NotImplementedError
24
+ end
25
+
26
+ private
27
+
28
+ def time
29
+ @time_source.call()
30
+ end
31
+
32
+ def scheduled_execution_plans(time)
33
+ with_error_handling([]) do
34
+ world.persistence.find_past_scheduled_plans(time)
35
+ end
36
+ end
37
+
38
+ def with_error_handling(error_retval = nil, &block)
39
+ block.call
40
+ rescue Exception => e
41
+ @logger.fatal e.backtrace.join("\n")
42
+ error_retval
43
+ end
44
+
45
+ def process(scheduled_plans, check_time)
46
+ processed_plan_uuids = []
47
+ scheduled_plans.each do |plan|
48
+ with_error_handling do
49
+ if !plan.start_before.nil? && plan.start_before < check_time
50
+ @logger.debug "Failing plan #{plan.execution_plan_uuid}"
51
+ plan.timeout
52
+ else
53
+ @logger.debug "Executing plan #{plan.execution_plan_uuid}"
54
+ plan.plan
55
+ plan.execute
56
+ end
57
+ processed_plan_uuids << plan.execution_plan_uuid
58
+ end
59
+ end
60
+ world.persistence.delete_scheduled_plans(:execution_plan_uuid => processed_plan_uuids) unless processed_plan_uuids.empty?
61
+ end
62
+
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,32 @@
1
+ module Dynflow
2
+ module Schedulers
3
+ class Polling < Abstract
4
+
5
+ def core_class
6
+ Dynflow::Schedulers::PollingCore
7
+ end
8
+
9
+ end
10
+
11
+ class PollingCore < AbstractCore
12
+ attr_reader :poll_interval
13
+
14
+ def configure(options)
15
+ super(options)
16
+ @poll_interval = options[:poll_interval]
17
+ end
18
+
19
+ def start
20
+ check_schedule
21
+ end
22
+
23
+ def check_schedule
24
+ check_time = time
25
+ plans = scheduled_execution_plans(check_time)
26
+ process plans, check_time
27
+
28
+ world.clock.ping(self, poll_interval, :check_schedule)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,8 @@
1
+ module Dynflow
2
+ module Serializers
3
+
4
+ require 'dynflow/serializers/abstract'
5
+ require 'dynflow/serializers/noop'
6
+
7
+ end
8
+ end
@@ -0,0 +1,15 @@
1
+ module Dynflow
2
+ module Serializers
3
+ class Abstract
4
+
5
+ def serialize(*args)
6
+ raise NotImplementedError
7
+ end
8
+
9
+ def deserialize(serialized_args)
10
+ raise NotImplementedError
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ module Dynflow
2
+ module Serializers
3
+ class Noop < Abstract
4
+
5
+ def serialize(*args)
6
+ args
7
+ end
8
+
9
+ def deserialize(serialized_args)
10
+ serialized_args
11
+ end
12
+
13
+ end
14
+ end
15
+ end
@@ -1,3 +1,3 @@
1
1
  module Dynflow
2
- VERSION = '0.8.1'
2
+ VERSION = '0.8.2'
3
3
  end
@@ -37,31 +37,16 @@ module Dynflow
37
37
  erb :worlds
38
38
  end
39
39
 
40
- post('/worlds/:id/ping') do |id|
41
- timeout = 5
42
- ping_response = world.ping(id, timeout).wait
43
- if ping_response.failed?
44
- response = "failed: #{ping_response.reason.message}"
45
- inactive_world_id = id
46
- else
47
- response = 'pong'
48
- end
49
- redirect(url "/worlds?notice=#{url_encode(response)}&inactive_world_id=#{inactive_world_id}")
40
+ post('/worlds/check') do
41
+ @worlds = world.coordinator.find_worlds
42
+ @validation_results = world.worlds_validity_check(params[:invalidate])
43
+ erb :worlds
50
44
  end
51
45
 
52
- post('/worlds/:id/invalidate') do |id|
53
- invalidated_world = world.coordinator.find_worlds(false, id: id).first
54
- unless invalidated_world
55
- response = "World #{id} not found"
56
- else
57
- begin
58
- world.invalidate(invalidated_world)
59
- response = "World #{invalidated_world.id} invalidated"
60
- rescue => e
61
- response = "World invalidation failed: #{e.message}"
62
- end
63
- end
64
- redirect(url "/worlds?notice=#{url_encode(response)}")
46
+ post('/worlds/:id/check') do |id|
47
+ @worlds = world.coordinator.find_worlds
48
+ @validation_results = world.worlds_validity_check(params[:invalidate], id: params[:id])
49
+ erb :worlds
65
50
  end
66
51
 
67
52
  get('/:id') do |id|
@@ -1,6 +1,14 @@
1
1
  module Dynflow
2
2
  module Web
3
3
  module ConsoleHelpers
4
+ def validation_result_css_class(result)
5
+ if result == :valid
6
+ "success"
7
+ else
8
+ "danger"
9
+ end
10
+ end
11
+
4
12
  def prettify_value(value)
5
13
  YAML.dump(value)
6
14
  end
@@ -107,6 +115,8 @@ module Dynflow
107
115
  "success"
108
116
  when :error
109
117
  "important"
118
+ else
119
+ "default"
110
120
  end
111
121
  end
112
122