dynflow 0.8.35 → 0.8.36
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +12 -11
- data/doc/pages/Gemfile +2 -0
- data/doc/pages/README.md +48 -0
- data/doc/pages/_config.yml +1 -1
- data/doc/pages/plugins/plantuml.rb +1 -1
- data/doc/pages/source/documentation/index.md +96 -1
- data/lib/dynflow/action.rb +13 -0
- data/lib/dynflow/delayed_plan.rb +13 -5
- data/lib/dynflow/execution_plan.rb +18 -0
- data/lib/dynflow/execution_plan/hooks.rb +91 -0
- data/lib/dynflow/execution_plan/steps/abstract.rb +13 -13
- data/lib/dynflow/persistence.rb +11 -0
- data/lib/dynflow/persistence_adapters/abstract.rb +8 -0
- data/lib/dynflow/persistence_adapters/sequel.rb +87 -28
- data/lib/dynflow/persistence_adapters/sequel_migrations/011_add_uuid_column.rb +61 -0
- data/lib/dynflow/persistence_adapters/sequel_migrations/012_add_delayed_plans_serialized_args.rb +8 -0
- data/lib/dynflow/persistence_adapters/sequel_migrations/013_add_action_columns.rb +16 -0
- data/lib/dynflow/persistence_adapters/sequel_migrations/014_add_step_columns.rb +13 -0
- data/lib/dynflow/persistence_adapters/sequel_migrations/015_add_execution_plan_columns.rb +18 -0
- data/lib/dynflow/serializable.rb +2 -1
- data/lib/dynflow/serializers/abstract.rb +34 -5
- data/lib/dynflow/version.rb +1 -1
- data/test/batch_sub_tasks_test.rb +3 -0
- data/test/concurrency_control_test.rb +6 -2
- data/test/execution_plan_hooks_test.rb +86 -0
- data/test/executor_test.rb +5 -2
- data/test/future_execution_test.rb +37 -0
- data/test/persistence_test.rb +144 -29
- metadata +11 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9d355c9b625deb22695548050c4d72ee3c43ce3a14e43488507173554c9c04d0
|
4
|
+
data.tar.gz: 63693d7793f8e1f12ff03d4b0b9ea3e9bbb1114f701cd92f470dd268f8dac732
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6760a74b0ec0f87e9ffb01a87f322b4151be4b1ddb578aa4ce56956f363a0a75db7bbcfbdf1eecc370dedc0b5ea1e182e4d85b86c22b411947b728ac531ed69f
|
7
|
+
data.tar.gz: 29a76f2faa3d93a24269c1cc233ca5f4cde5865e5c454dfb945b2605a184fc29203b7bb4f5a99100695557332bf6a0690f999ed4026dfc9570877f6085d8b374
|
data/.travis.yml
CHANGED
@@ -3,22 +3,23 @@ language:
|
|
3
3
|
- ruby
|
4
4
|
|
5
5
|
rvm:
|
6
|
-
- "2.
|
7
|
-
- "2.1.5"
|
8
|
-
- "2.2.0"
|
6
|
+
- "2.2.2"
|
9
7
|
- "2.3.1"
|
10
8
|
- "2.4.0"
|
9
|
+
- "2.5.0"
|
11
10
|
|
12
11
|
env:
|
13
12
|
global:
|
14
|
-
- "TESTOPTS=--verbose"
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
-
|
19
|
-
|
20
|
-
-
|
21
|
-
|
13
|
+
- "TESTOPTS=--verbose DB=postgresql DB_CONN_STRING=postgres://postgres@localhost/travis_ci_test"
|
14
|
+
|
15
|
+
matrix:
|
16
|
+
include:
|
17
|
+
- rvm: "2.4.0"
|
18
|
+
env: "DB=mysql DB_CONN_STRING=mysql2://root@localhost/travis_ci_test"
|
19
|
+
- rvm: "2.4.0"
|
20
|
+
env: "DB=sqlite3 DB_CONN_STRING=sqlite:/"
|
21
|
+
- rvm: "2.4.0"
|
22
|
+
env: "CONCURRENT_RUBY_EXT=true"
|
22
23
|
|
23
24
|
install:
|
24
25
|
- test/prepare_travis_env.sh
|
data/doc/pages/Gemfile
CHANGED
data/doc/pages/README.md
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# Building dynflow.github.io
|
2
|
+
|
3
|
+
1. Clone the `dynflow.github.io` to the public directory
|
4
|
+
|
5
|
+
```
|
6
|
+
git clone github.com:dynflow/dynflow.github.io public --origin upstream
|
7
|
+
```
|
8
|
+
|
9
|
+
2. Add your fork
|
10
|
+
|
11
|
+
```
|
12
|
+
cd public
|
13
|
+
git remote add origin github.com:$MYUSERNAME/dynflow.github.io
|
14
|
+
cd ..
|
15
|
+
```
|
16
|
+
|
17
|
+
2. Install the dependencies
|
18
|
+
|
19
|
+
```
|
20
|
+
bundle install
|
21
|
+
```
|
22
|
+
|
23
|
+
3. Make sure the public repository is in sync with upstream
|
24
|
+
|
25
|
+
```
|
26
|
+
cd public
|
27
|
+
git fetch upstream master
|
28
|
+
git reset --hard upstream/master
|
29
|
+
git clean -f
|
30
|
+
cd ..
|
31
|
+
```
|
32
|
+
|
33
|
+
4. Build new version of the pages
|
34
|
+
|
35
|
+
```
|
36
|
+
bundle exec jekyll build
|
37
|
+
```
|
38
|
+
|
39
|
+
5. Commit and push the updated version
|
40
|
+
|
41
|
+
```
|
42
|
+
cd public
|
43
|
+
git add -A
|
44
|
+
git commit -m Update
|
45
|
+
git push origin master
|
46
|
+
```
|
47
|
+
|
48
|
+
6. Send us a PR
|
data/doc/pages/_config.yml
CHANGED
@@ -24,7 +24,7 @@ module Jekyll
|
|
24
24
|
folder = "/images/plantuml/"
|
25
25
|
create_tmp_folder(tmproot, folder)
|
26
26
|
|
27
|
-
code =
|
27
|
+
code = nodelist.join + background_color
|
28
28
|
filename = Digest::MD5.hexdigest(code) + ".png"
|
29
29
|
filepath = tmproot + folder + filename
|
30
30
|
if !File.exist?(filepath)
|
@@ -69,6 +69,8 @@ Dynflow has been developed to be able to support orchestration of services in th
|
|
69
69
|
talk to each other, which is helpful for production and
|
70
70
|
high-availability setups,
|
71
71
|
having multiple worlds on different hosts handle the execution of the execution plans.
|
72
|
+
If you're still confused and come from RoR world, think about it as similar thing
|
73
|
+
that is Rails object for Ruby on Rails framework.
|
72
74
|
|
73
75
|
## Examples
|
74
76
|
|
@@ -984,7 +986,72 @@ to the chain of middleware execution.
|
|
984
986
|
### Sub-plans
|
985
987
|
|
986
988
|
- *when to use?*
|
987
|
-
|
989
|
+
|
990
|
+
To use sub-plans, you must include the `Dynflow::Action::WithSubPlans` module
|
991
|
+
and override the `create_sub_plans` method. Inside the `create_sub_plans`
|
992
|
+
method, you use the `trigger` method to create sub-tasks that will be executed
|
993
|
+
in no particular order during the run phase. The parent task will wait for the
|
994
|
+
sub-tasks to finish without blocking a thread in a pool while waiting.
|
995
|
+
|
996
|
+
```rb
|
997
|
+
class MyAction < Actions::EntryAction
|
998
|
+
include Dynflow::Action::WithSubPlans
|
999
|
+
|
1000
|
+
...
|
1001
|
+
|
1002
|
+
def create_sub_plans
|
1003
|
+
[
|
1004
|
+
trigger(Actions::OtherAction, action_param1, action_opts),
|
1005
|
+
trigger(Actions::AnotherAction)
|
1006
|
+
]
|
1007
|
+
end
|
1008
|
+
end
|
1009
|
+
```
|
1010
|
+
|
1011
|
+
### Execution plan hooks
|
1012
|
+
|
1013
|
+
Dynflow allows actions to hook into the lifecycle of their execution plans. To
|
1014
|
+
use the hooks, the user has to define a method on the action and register it as
|
1015
|
+
a hook. Currently there are hook events for every execution plan's state which
|
1016
|
+
are executed when the execution plan transitions into the state. Additionally
|
1017
|
+
there are two more hook events, `failure` and `success` which are run when the
|
1018
|
+
execution plan transitions into the `stopped` state with `error` or `success`
|
1019
|
+
result.
|
1020
|
+
|
1021
|
+
Methods can be registered using `execution_plan_hooks.use` call, providing the
|
1022
|
+
method name as `Symbol` and optionally a `:on => HOOK_EVENT` parameter, where
|
1023
|
+
`HOOK_EVENT` can be one of the hook events or an array of them. In case the
|
1024
|
+
optional parameter is not provided, the method is executed on every state
|
1025
|
+
change. Similarly, for example inherited, hooks can be disabled by calling
|
1026
|
+
`execution_plan_hooks.do_not_use`, taking the same arguments. Hooks defined on
|
1027
|
+
an action are inherited when the action is sub-classed.
|
1028
|
+
|
1029
|
+
The hooks are executed for every action in the execution plan and the order of
|
1030
|
+
execution is not guaranteed.
|
1031
|
+
|
1032
|
+
```rb
|
1033
|
+
class MyAction < Actions::EntryAction
|
1034
|
+
# Sets up a hook to call #state_change method when the execution plan changes
|
1035
|
+
# its state
|
1036
|
+
execution_plan_hooks.use :state_change
|
1037
|
+
|
1038
|
+
# Sets up a hook to call #success_notification method when the execution plan
|
1039
|
+
# finishes successfully,
|
1040
|
+
execution_plan_hooks.use :success_notification, :on => :success
|
1041
|
+
|
1042
|
+
# Disables running #state_change method when the execution plan starts or
|
1043
|
+
# finishes planning
|
1044
|
+
execution_plan_hooks.do_not_use :state_change, :on => [:planning, :planned]
|
1045
|
+
|
1046
|
+
def state_change(_execution_plan)
|
1047
|
+
# Do something on every state change
|
1048
|
+
end
|
1049
|
+
|
1050
|
+
def success_notification(_execution_plan)
|
1051
|
+
# Display a notification
|
1052
|
+
end
|
1053
|
+
end
|
1054
|
+
```
|
988
1055
|
|
989
1056
|
## How it works TODO
|
990
1057
|
|
@@ -1319,6 +1386,34 @@ executor is actively working on what execution plan: the executor is
|
|
1319
1386
|
not allowed to start executing the unless it has successfully acquired
|
1320
1387
|
a lock for it.
|
1321
1388
|
|
1389
|
+
### Singleton Actions
|
1390
|
+
Dynflow has a special module for actions of which there should be only
|
1391
|
+
one instance active at a time. This module provides a number of methods
|
1392
|
+
for managing the action's locks as well as a middleware for automatic
|
1393
|
+
locking.
|
1394
|
+
|
1395
|
+
It works in the following way. The middleware tries to acquire the
|
1396
|
+
lock for this action, which is owned by the execution plan. If another
|
1397
|
+
action already holds the lock, it fill fail and the execution plan
|
1398
|
+
will transition to stopped-error state. Having obtained the lock,
|
1399
|
+
the action goes through the planning as usually. In run phase, the
|
1400
|
+
middleware checks if the execution plan still owns the lock for the action.
|
1401
|
+
If the execution plan holds the lock or there is no lock at all and
|
1402
|
+
the action manages to acquire it again, the execution proceeds. If the
|
1403
|
+
lock is held by another execution plan, the current one fails. Unlocking
|
1404
|
+
can be either done manually from within the action or can be left to the
|
1405
|
+
execution plan. The execution plan unlocks all locks it holds whenever it
|
1406
|
+
transitions to paused or stopped state.
|
1407
|
+
|
1408
|
+
All that is needed to make an action a singleton one is including the module
|
1409
|
+
into it.
|
1410
|
+
|
1411
|
+
```ruby
|
1412
|
+
class ExampleSingletonAction < ::Dynflow::Action
|
1413
|
+
include ::Dynflow::Action::Singleton
|
1414
|
+
end
|
1415
|
+
```
|
1416
|
+
|
1322
1417
|
### Thread-pools TODO
|
1323
1418
|
|
1324
1419
|
- *how it works now*
|
data/lib/dynflow/action.rb
CHANGED
@@ -34,6 +34,7 @@ module Dynflow
|
|
34
34
|
|
35
35
|
def self.inherited(child)
|
36
36
|
children[child.name] = child
|
37
|
+
child.inherit_execution_plan_hooks(execution_plan_hooks.dup)
|
37
38
|
super child
|
38
39
|
end
|
39
40
|
|
@@ -45,6 +46,14 @@ module Dynflow
|
|
45
46
|
@middleware ||= Middleware::Register.new
|
46
47
|
end
|
47
48
|
|
49
|
+
def self.execution_plan_hooks
|
50
|
+
@execution_plan_hooks ||= ExecutionPlan::Hooks::Register.new
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.inherit_execution_plan_hooks(hooks)
|
54
|
+
@execution_plan_hooks = hooks
|
55
|
+
end
|
56
|
+
|
48
57
|
# FIND define subscriptions in world independent on action's classes,
|
49
58
|
# limited only by in/output formats
|
50
59
|
# @return [nil, Class] a child of Action
|
@@ -364,6 +373,10 @@ module Dynflow
|
|
364
373
|
end
|
365
374
|
|
366
375
|
def self.new_from_hash(hash, world)
|
376
|
+
hash.delete(:output) if hash[:output].nil?
|
377
|
+
unless hash[:execution_plan_uuid].nil?
|
378
|
+
hash[:execution_plan_id] = hash[:execution_plan_uuid]
|
379
|
+
end
|
367
380
|
new(hash, world)
|
368
381
|
end
|
369
382
|
|
data/lib/dynflow/delayed_plan.rb
CHANGED
@@ -38,23 +38,31 @@ module Dynflow
|
|
38
38
|
|
39
39
|
def cancel
|
40
40
|
error("Delayed task cancelled", "Delayed task cancelled")
|
41
|
-
@world.persistence.delete_delayed_plans(:execution_plan_uuid =>
|
41
|
+
@world.persistence.delete_delayed_plans(:execution_plan_uuid => @execution_plan_uuid)
|
42
42
|
return true
|
43
43
|
end
|
44
44
|
|
45
45
|
def execute(future = Concurrent.future)
|
46
|
-
@world.execute(
|
47
|
-
::Dynflow::World::Triggered[
|
46
|
+
@world.execute(@execution_plan_uuid, future)
|
47
|
+
::Dynflow::World::Triggered[@execution_plan_uuid, future]
|
48
48
|
end
|
49
49
|
|
50
50
|
def to_hash
|
51
51
|
recursive_to_hash :execution_plan_uuid => @execution_plan_uuid,
|
52
|
-
:start_at =>
|
53
|
-
:start_before =>
|
52
|
+
:start_at => @start_at,
|
53
|
+
:start_before => @start_before,
|
54
54
|
:serialized_args => @args_serializer.serialized_args,
|
55
55
|
:args_serializer => @args_serializer.class.name
|
56
56
|
end
|
57
57
|
|
58
|
+
# Retrieves arguments from the serializer
|
59
|
+
#
|
60
|
+
# @return [Array] array of the original arguments
|
61
|
+
def args
|
62
|
+
@args_serializer.perform_deserialization! if @args_serializer.args.nil?
|
63
|
+
@args_serializer.args
|
64
|
+
end
|
65
|
+
|
58
66
|
# @api private
|
59
67
|
def self.new_from_hash(world, hash, *args)
|
60
68
|
serializer = Utils.constantize(hash[:args_serializer]).new(nil, hash[:serialized_args])
|
@@ -51,6 +51,8 @@ module Dynflow
|
|
51
51
|
@states ||= [:pending, :scheduled, :planning, :planned, :running, :paused, :stopped]
|
52
52
|
end
|
53
53
|
|
54
|
+
require 'dynflow/execution_plan/hooks'
|
55
|
+
|
54
56
|
def self.results
|
55
57
|
@results ||= [:pending, :success, :warning, :error]
|
56
58
|
end
|
@@ -109,6 +111,7 @@ module Dynflow
|
|
109
111
|
end
|
110
112
|
|
111
113
|
def update_state(state)
|
114
|
+
hooks_to_run = [state]
|
112
115
|
original = self.state
|
113
116
|
case self.state = state
|
114
117
|
when :planning
|
@@ -117,6 +120,7 @@ module Dynflow
|
|
117
120
|
@ended_at = Time.now
|
118
121
|
@real_time = @ended_at - @started_at unless @started_at.nil?
|
119
122
|
@execution_time = compute_execution_time
|
123
|
+
hooks_to_run << (error? ? :failure : :success)
|
120
124
|
unlock_all_singleton_locks!
|
121
125
|
when :paused
|
122
126
|
unlock_all_singleton_locks!
|
@@ -126,6 +130,20 @@ module Dynflow
|
|
126
130
|
logger.debug format('%13s %s %9s >> %9s',
|
127
131
|
'ExecutionPlan', id, original, state)
|
128
132
|
self.save
|
133
|
+
hooks_to_run.each { |kind| run_hooks kind }
|
134
|
+
end
|
135
|
+
|
136
|
+
def run_hooks(state)
|
137
|
+
records = persistence.load_actions_attributes(@id, [:id, :class]).select do |action|
|
138
|
+
Utils.constantize(action[:class])
|
139
|
+
.execution_plan_hooks
|
140
|
+
.on(state).any?
|
141
|
+
end
|
142
|
+
action_ids = records.compact.map { |record| record[:id] }
|
143
|
+
return if action_ids.empty?
|
144
|
+
persistence.load_actions(self, action_ids).each do |action|
|
145
|
+
action.class.execution_plan_hooks.run(self, action, state)
|
146
|
+
end
|
129
147
|
end
|
130
148
|
|
131
149
|
def result
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module Dynflow
|
2
|
+
class ExecutionPlan
|
3
|
+
module Hooks
|
4
|
+
|
5
|
+
HOOK_KINDS = (ExecutionPlan.states + [:success, :failure]).freeze
|
6
|
+
|
7
|
+
# A register holding information about hook classes and events
|
8
|
+
# which should trigger the hooks.
|
9
|
+
#
|
10
|
+
# @attr_reader hooks [Hash<Class, Set<Symbol>>] a hash mapping hook classes to events which should trigger the hooks
|
11
|
+
class Register
|
12
|
+
attr_reader :hooks
|
13
|
+
|
14
|
+
def initialize(hooks = {})
|
15
|
+
@hooks = hooks
|
16
|
+
end
|
17
|
+
|
18
|
+
# Sets a hook to be run on certain events
|
19
|
+
#
|
20
|
+
# @param class_name [Class] class of the hook to be run
|
21
|
+
# @param on [Symbol, Array<Symbol>] when should the hook be run, one of {HOOK_KINDS}
|
22
|
+
# @return [void]
|
23
|
+
def use(class_name, on: HOOK_KINDS)
|
24
|
+
on = Array[on] unless on.kind_of?(Array)
|
25
|
+
validate_kinds!(on)
|
26
|
+
if hooks[class_name]
|
27
|
+
hooks[class_name] += on
|
28
|
+
else
|
29
|
+
hooks[class_name] = on
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Disables a hook from being run on certain events
|
34
|
+
#
|
35
|
+
# @param class_name [Class] class of the hook to disable
|
36
|
+
# @param on [Symbol, Array<Symbol>] when should the hook be disabled, one of {HOOK_KINDS}
|
37
|
+
# @return [void]
|
38
|
+
def do_not_use(class_name, on: HOOK_KINDS)
|
39
|
+
on = Array[on] unless on.kind_of?(Array)
|
40
|
+
validate_kinds!(on)
|
41
|
+
if hooks[class_name]
|
42
|
+
hooks[class_name] -= on
|
43
|
+
hooks.delete(class_name) if hooks[class_name].empty?
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Performs a deep clone of the hooks register
|
48
|
+
#
|
49
|
+
# @return [Register] new deeply cloned register
|
50
|
+
def dup
|
51
|
+
new_hooks = hooks.reduce({}) do |acc, (key, value)|
|
52
|
+
acc.update(key => value.dup)
|
53
|
+
end
|
54
|
+
self.class.new(new_hooks)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Runs the registered hooks
|
58
|
+
#
|
59
|
+
# @param execution_plan [ExecutionPlan] the execution plan which triggered the hooks
|
60
|
+
# @param action [Action] the action which triggered the hooks
|
61
|
+
# @param kind [Symbol] the kind of hooks to run, one of {HOOK_KINDS}
|
62
|
+
def run(execution_plan, action, kind)
|
63
|
+
on(kind).each do |hook|
|
64
|
+
begin
|
65
|
+
action.send(hook, execution_plan)
|
66
|
+
rescue => e
|
67
|
+
execution_plan.logger.error "Failed to run hook '#{hook}' for action '#{action.class}'"
|
68
|
+
execution_plan.logger.debug e
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns which hooks should be run on certain event.
|
74
|
+
#
|
75
|
+
# @param kind [Symbol] what kind of hook are we looking for
|
76
|
+
# @return [Array<Class>] list of hook classes to execute
|
77
|
+
def on(kind)
|
78
|
+
hooks.select { |_key, on| on.include? kind }.keys
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def validate_kinds!(kinds)
|
84
|
+
kinds.each do |kind|
|
85
|
+
raise "Unknown hook kind '#{kind}'" unless HOOK_KINDS.include?(kind)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|