dynflow 0.8.35 → 0.8.36
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 +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
|