dynflow 0.8.1 → 0.8.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/doc/pages/source/documentation/index.md +47 -12
- data/dynflow.gemspec +2 -2
- data/examples/future_execution.rb +73 -0
- data/lib/dynflow.rb +4 -1
- data/lib/dynflow/action.rb +15 -0
- data/lib/dynflow/config.rb +15 -1
- data/lib/dynflow/coordinator.rb +7 -0
- data/lib/dynflow/execution_plan.rb +15 -3
- data/lib/dynflow/execution_plan/steps/plan_step.rb +5 -1
- data/lib/dynflow/middleware.rb +4 -0
- data/lib/dynflow/middleware/stack.rb +1 -1
- data/lib/dynflow/middleware/world.rb +1 -1
- data/lib/dynflow/persistence.rb +19 -0
- data/lib/dynflow/persistence_adapters/abstract.rb +16 -0
- data/lib/dynflow/persistence_adapters/sequel.rb +31 -3
- data/lib/dynflow/persistence_adapters/sequel_migrations/006_fix_data_length.rb +17 -0
- data/lib/dynflow/persistence_adapters/sequel_migrations/007_future_execution.rb +13 -0
- data/lib/dynflow/scheduled_plan.rb +65 -0
- data/lib/dynflow/schedulers.rb +9 -0
- data/lib/dynflow/schedulers/abstract.rb +37 -0
- data/lib/dynflow/schedulers/abstract_core.rb +65 -0
- data/lib/dynflow/schedulers/polling.rb +32 -0
- data/lib/dynflow/serializers.rb +8 -0
- data/lib/dynflow/serializers/abstract.rb +15 -0
- data/lib/dynflow/serializers/noop.rb +15 -0
- data/lib/dynflow/version.rb +1 -1
- data/lib/dynflow/web/console.rb +8 -23
- data/lib/dynflow/web/console_helpers.rb +10 -0
- data/lib/dynflow/world.rb +99 -24
- data/test/abnormal_states_recovery_test.rb +64 -0
- data/test/future_execution_test.rb +114 -0
- data/test/middleware_test.rb +8 -2
- data/test/support/middleware_example.rb +11 -0
- data/test/test_helper.rb +1 -0
- data/web/views/show.erb +11 -0
- data/web/views/worlds.erb +19 -3
- 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,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
|
data/lib/dynflow/version.rb
CHANGED
data/lib/dynflow/web/console.rb
CHANGED
@@ -37,31 +37,16 @@ module Dynflow
|
|
37
37
|
erb :worlds
|
38
38
|
end
|
39
39
|
|
40
|
-
post('/worlds
|
41
|
-
|
42
|
-
|
43
|
-
|
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/
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
|