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