dynflow 0.8.1 → 0.8.2

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