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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 290e1bf999cae6f5ced255f29c2b62bc5d1fb53d75a0184ca514afbd3deb9c04
4
- data.tar.gz: 7e4bdad4c493342902070fa5f694569ff3cd1906c648bc403b47dccb03654646
3
+ metadata.gz: 9d355c9b625deb22695548050c4d72ee3c43ce3a14e43488507173554c9c04d0
4
+ data.tar.gz: 63693d7793f8e1f12ff03d4b0b9ea3e9bbb1114f701cd92f470dd268f8dac732
5
5
  SHA512:
6
- metadata.gz: 50250e41adda66f5f8069378d916fa1a58771076420806aa9185bbd2467a36acb038aeda64f3d7757c43801796c2507e23e062307bb40e9fd5ddaed18c0a5236
7
- data.tar.gz: 0aeae17937111257652845446f8ea5726d735651a57dd3d915f74e943a785566eb9340b5e1356e45bac86c2fe15033dff2e6b8fab50ae17aa05abe2bc32e4749
6
+ metadata.gz: 6760a74b0ec0f87e9ffb01a87f322b4151be4b1ddb578aa4ce56956f363a0a75db7bbcfbdf1eecc370dedc0b5ea1e182e4d85b86c22b411947b728ac531ed69f
7
+ data.tar.gz: 29a76f2faa3d93a24269c1cc233ca5f4cde5865e5c454dfb945b2605a184fc29203b7bb4f5a99100695557332bf6a0690f999ed4026dfc9570877f6085d8b374
@@ -3,22 +3,23 @@ language:
3
3
  - ruby
4
4
 
5
5
  rvm:
6
- - "2.0.0"
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
- matrix:
16
- - "DB=mysql DB_CONN_STRING=mysql2://root@localhost/travis_ci_test CONCURRENT_RUBY_EXT=true"
17
- - "DB=postgresql DB_CONN_STRING=postgres://postgres@localhost/travis_ci_test CONCURRENT_RUBY_EXT=true"
18
- - "DB=sqlite3 CONCURRENT_RUBY_EXT=true"
19
- - "DB=mysql DB_CONN_STRING=mysql2://root@localhost/travis_ci_test CONCURRENT_RUBY_EXT=false"
20
- - "DB=postgresql DB_CONN_STRING=postgres://postgres@localhost/travis_ci_test CONCURRENT_RUBY_EXT=false"
21
- - "DB=sqlite3 CONCURRENT_RUBY_EXT=false"
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
@@ -5,3 +5,5 @@ gem 'jekyll'
5
5
  gem 'pry'
6
6
  gem 'ruby-nuggets' # require by tags plugin
7
7
  gem 'therubyracer'
8
+ gem 'redcarpet'
9
+ gem 'pygments.rb'
@@ -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
@@ -7,7 +7,7 @@ highlighter: pygments
7
7
 
8
8
  source: ./source
9
9
  destination: ./public
10
- plugins: ./plugins
10
+ plugins_dir: ./plugins
11
11
  include:
12
12
  - .nojekyll
13
13
 
@@ -24,7 +24,7 @@ module Jekyll
24
24
  folder = "/images/plantuml/"
25
25
  create_tmp_folder(tmproot, folder)
26
26
 
27
- code = @nodelist.join + background_color
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
- - *how to use?*
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*
@@ -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
 
@@ -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 => execution_plan.id)
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(execution_plan.id, future)
47
- ::Dynflow::World::Triggered[execution_plan.id, future]
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 => time_to_str(@start_at),
53
- :start_before => time_to_str(@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