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