dynflow 0.7.9 → 0.8.0

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.
Files changed (118) hide show
  1. data/.gitignore +2 -0
  2. data/.travis.yml +16 -1
  3. data/Gemfile +13 -1
  4. data/doc/pages/source/_drafts/2015-03-01-new-documentation.markdown +10 -0
  5. data/doc/pages/source/_includes/menu.html +1 -0
  6. data/doc/pages/source/_includes/menu_right.html +1 -1
  7. data/doc/pages/source/_sass/_bootstrap-variables.sass +1 -0
  8. data/doc/pages/source/_sass/_style.scss +4 -0
  9. data/doc/pages/source/blog/index.html +12 -0
  10. data/doc/pages/source/documentation/index.md +330 -5
  11. data/dynflow.gemspec +3 -1
  12. data/examples/example_helper.rb +18 -11
  13. data/examples/orchestrate_evented.rb +2 -1
  14. data/examples/remote_executor.rb +53 -20
  15. data/lib/dynflow.rb +16 -6
  16. data/lib/dynflow/action/suspended.rb +1 -1
  17. data/lib/dynflow/action/with_sub_plans.rb +3 -6
  18. data/lib/dynflow/actor.rb +56 -0
  19. data/lib/dynflow/clock.rb +43 -38
  20. data/lib/dynflow/config.rb +107 -0
  21. data/lib/dynflow/connectors.rb +7 -0
  22. data/lib/dynflow/connectors/abstract.rb +41 -0
  23. data/lib/dynflow/connectors/database.rb +175 -0
  24. data/lib/dynflow/connectors/direct.rb +71 -0
  25. data/lib/dynflow/coordinator.rb +280 -0
  26. data/lib/dynflow/coordinator_adapters.rb +8 -0
  27. data/lib/dynflow/coordinator_adapters/abstract.rb +28 -0
  28. data/lib/dynflow/coordinator_adapters/sequel.rb +29 -0
  29. data/lib/dynflow/dispatcher.rb +58 -0
  30. data/lib/dynflow/dispatcher/abstract.rb +14 -0
  31. data/lib/dynflow/dispatcher/client_dispatcher.rb +139 -0
  32. data/lib/dynflow/dispatcher/executor_dispatcher.rb +86 -0
  33. data/lib/dynflow/errors.rb +7 -1
  34. data/lib/dynflow/execution_history.rb +46 -0
  35. data/lib/dynflow/execution_plan.rb +19 -15
  36. data/lib/dynflow/executors.rb +0 -1
  37. data/lib/dynflow/executors/abstract.rb +5 -10
  38. data/lib/dynflow/executors/parallel.rb +16 -13
  39. data/lib/dynflow/executors/parallel/core.rb +76 -78
  40. data/lib/dynflow/executors/parallel/execution_plan_manager.rb +4 -5
  41. data/lib/dynflow/executors/parallel/pool.rb +22 -52
  42. data/lib/dynflow/executors/parallel/running_steps_manager.rb +9 -2
  43. data/lib/dynflow/executors/parallel/worker.rb +5 -10
  44. data/lib/dynflow/persistence.rb +14 -0
  45. data/lib/dynflow/persistence_adapters/abstract.rb +14 -3
  46. data/lib/dynflow/persistence_adapters/sequel.rb +142 -38
  47. data/lib/dynflow/persistence_adapters/sequel_migrations/004_coordinator_records.rb +14 -0
  48. data/lib/dynflow/persistence_adapters/sequel_migrations/005_envelopes.rb +14 -0
  49. data/lib/dynflow/round_robin.rb +37 -0
  50. data/lib/dynflow/serializable.rb +1 -2
  51. data/lib/dynflow/serializer.rb +46 -0
  52. data/lib/dynflow/testing/dummy_executor.rb +2 -2
  53. data/lib/dynflow/testing/dummy_world.rb +1 -1
  54. data/lib/dynflow/transaction_adapters/abstract.rb +0 -5
  55. data/lib/dynflow/transaction_adapters/active_record.rb +0 -10
  56. data/lib/dynflow/version.rb +1 -1
  57. data/lib/dynflow/web.rb +26 -0
  58. data/lib/dynflow/web/console.rb +108 -0
  59. data/lib/dynflow/web/console_helpers.rb +158 -0
  60. data/lib/dynflow/web/filtering_helpers.rb +85 -0
  61. data/lib/dynflow/web/world_helpers.rb +9 -0
  62. data/lib/dynflow/web_console.rb +3 -310
  63. data/lib/dynflow/world.rb +188 -119
  64. data/test/abnormal_states_recovery_test.rb +152 -0
  65. data/test/action_test.rb +2 -3
  66. data/test/clock_test.rb +1 -5
  67. data/test/coordinator_test.rb +152 -0
  68. data/test/dispatcher_test.rb +146 -0
  69. data/test/execution_plan_test.rb +2 -1
  70. data/test/executor_test.rb +534 -612
  71. data/test/middleware_test.rb +4 -4
  72. data/test/persistence_test.rb +17 -0
  73. data/test/prepare_travis_env.sh +35 -0
  74. data/test/rescue_test.rb +5 -3
  75. data/test/round_robin_test.rb +28 -0
  76. data/test/support/code_workflow_example.rb +0 -73
  77. data/test/support/dummy_example.rb +130 -0
  78. data/test/support/test_execution_log.rb +41 -0
  79. data/test/test_helper.rb +222 -116
  80. data/test/testing_test.rb +10 -10
  81. data/test/web_console_test.rb +3 -3
  82. data/test/world_test.rb +23 -0
  83. data/web/assets/images/logo-square.png +0 -0
  84. data/web/assets/stylesheets/application.css +9 -0
  85. data/web/assets/vendor/bootstrap/config.json +429 -0
  86. data/web/assets/vendor/bootstrap/css/bootstrap-theme.css +479 -0
  87. data/web/assets/vendor/bootstrap/css/bootstrap-theme.min.css +10 -0
  88. data/web/assets/vendor/bootstrap/css/bootstrap.css +5377 -4980
  89. data/web/assets/vendor/bootstrap/css/bootstrap.min.css +9 -8
  90. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.eot +0 -0
  91. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.svg +288 -0
  92. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf +0 -0
  93. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff +0 -0
  94. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff2 +0 -0
  95. data/web/assets/vendor/bootstrap/js/bootstrap.js +1674 -1645
  96. data/web/assets/vendor/bootstrap/js/bootstrap.min.js +11 -5
  97. data/web/views/execution_history.erb +17 -0
  98. data/web/views/index.erb +4 -6
  99. data/web/views/layout.erb +44 -8
  100. data/web/views/show.erb +4 -5
  101. data/web/views/worlds.erb +26 -0
  102. metadata +116 -23
  103. checksums.yaml +0 -15
  104. data/lib/dynflow/daemon.rb +0 -30
  105. data/lib/dynflow/executors/remote_via_socket.rb +0 -43
  106. data/lib/dynflow/executors/remote_via_socket/core.rb +0 -184
  107. data/lib/dynflow/future.rb +0 -173
  108. data/lib/dynflow/listeners.rb +0 -7
  109. data/lib/dynflow/listeners/abstract.rb +0 -17
  110. data/lib/dynflow/listeners/serialization.rb +0 -77
  111. data/lib/dynflow/listeners/socket.rb +0 -117
  112. data/lib/dynflow/micro_actor.rb +0 -102
  113. data/lib/dynflow/simple_world.rb +0 -19
  114. data/test/remote_via_socket_test.rb +0 -170
  115. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.css +0 -1109
  116. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.min.css +0 -9
  117. data/web/assets/vendor/bootstrap/img/glyphicons-halflings-white.png +0 -0
  118. data/web/assets/vendor/bootstrap/img/glyphicons-halflings.png +0 -0
@@ -0,0 +1,46 @@
1
+ require 'algebrick/serializer'
2
+
3
+ module Dynflow
4
+ def self.serializer
5
+ @serializer ||= Serializer.new
6
+ end
7
+
8
+ class Serializer < Algebrick::Serializer
9
+
10
+ ARBITRARY_TYPE_KEY = :class
11
+ MARSHAL_KEY = :marshaled
12
+
13
+ protected
14
+
15
+ def parse_other(other, options = {})
16
+ if Hash === other
17
+ if (marshal_value = other[MARSHAL_KEY] || other[MARSHAL_KEY.to_s])
18
+ return Marshal.load(Base64.strict_decode64(marshal_value))
19
+ end
20
+
21
+ if (type_name = other[ARBITRARY_TYPE_KEY] || other[ARBITRARY_TYPE_KEY.to_s])
22
+ type = type_name.constantize rescue nil
23
+ if type && type.respond_to?(:from_hash)
24
+ return type.from_hash other
25
+ end
26
+ end
27
+ end
28
+
29
+ return other
30
+ end
31
+
32
+ def generate_other(object, options = {})
33
+ hash = case
34
+ when object.respond_to?(:to_h)
35
+ object.to_h
36
+ when object.respond_to?(:to_hash)
37
+ object.to_hash
38
+ else
39
+ { ARBITRARY_TYPE_KEY => object.class.to_s,
40
+ MARSHAL_KEY => Base64.strict_encode64(Marshal.dump(object)) }
41
+ end
42
+ raise "Missing #{ARBITRARY_TYPE_KEY} key in #{hash.inspect}" unless hash.key?(ARBITRARY_TYPE_KEY)
43
+ hash
44
+ end
45
+ end
46
+ end
@@ -8,7 +8,7 @@ module Dynflow
8
8
  @events_to_process = []
9
9
  end
10
10
 
11
- def event(execution_plan_id, step_id, event, future = Future.new)
11
+ def event(execution_plan_id, step_id, event, future = Concurrent.future)
12
12
  @events_to_process << [execution_plan_id, step_id, event, future]
13
13
  end
14
14
 
@@ -17,7 +17,7 @@ module Dynflow
17
17
  events = @events_to_process.dup
18
18
  clear
19
19
  events.each do |execution_plan_id, step_id, event, future|
20
- future.resolve true
20
+ future.success true
21
21
  if event && world.action.state != :suspended
22
22
  return false
23
23
  end
@@ -30,7 +30,7 @@ module Dynflow
30
30
  []
31
31
  end
32
32
 
33
- def event(execution_plan_id, step_id, event, future = Future.new)
33
+ def event(execution_plan_id, step_id, event, future = Concurrent.future)
34
34
  executor.event execution_plan_id, step_id, event, future
35
35
  end
36
36
 
@@ -16,11 +16,6 @@ module Dynflow
16
16
  def cleanup
17
17
  # override if needed
18
18
  end
19
-
20
- # Called after World instantiation, it can be used to check Dynflow configuration etc.
21
- def check(world)
22
- # override if needed
23
- end
24
19
  end
25
20
  end
26
21
  end
@@ -12,16 +12,6 @@ module Dynflow
12
12
  def cleanup
13
13
  ::ActiveRecord::Base.clear_active_connections!
14
14
  end
15
-
16
- def check(world)
17
- # missing reader in ConnectionPool
18
- ar_pool_size = ::ActiveRecord::Base.connection_pool.instance_variable_get(:@size)
19
- if (world.options[:pool_size] / 2.0) > ar_pool_size
20
- world.logger.warn 'Consider increasing ActiveRecord::Base.connection_pool size, ' +
21
- "it's #{ar_pool_size} but there is #{world.options[:pool_size]} " +
22
- 'threads in Dynflow pool.'
23
- end
24
- end
25
15
  end
26
16
  end
27
17
  end
@@ -1,3 +1,3 @@
1
1
  module Dynflow
2
- VERSION = '0.7.9'
2
+ VERSION = '0.8.0'
3
3
  end
@@ -0,0 +1,26 @@
1
+ require 'dynflow'
2
+ require 'pp'
3
+ require 'sinatra/base'
4
+ require 'yaml'
5
+
6
+ module Dynflow
7
+ module Web
8
+
9
+ def self.setup(&block)
10
+ console = Sinatra.new(Web::Console) { instance_exec(&block)}
11
+ Rack::Builder.app do
12
+ run Rack::URLMap.new('/' => console)
13
+ end
14
+ end
15
+
16
+ def self.web_dir(sub_dir)
17
+ web_dir = File.join(File.expand_path('../../../web', __FILE__))
18
+ File.join(web_dir, sub_dir)
19
+ end
20
+
21
+ require 'dynflow/web/filtering_helpers'
22
+ require 'dynflow/web/world_helpers'
23
+ require 'dynflow/web/console_helpers'
24
+ require 'dynflow/web/console'
25
+ end
26
+ end
@@ -0,0 +1,108 @@
1
+
2
+ module Dynflow
3
+ module Web
4
+ class Console < Sinatra::Base
5
+
6
+ set :public_folder, Web.web_dir('assets')
7
+ set :views, Web.web_dir('views')
8
+ set :per_page, 10
9
+
10
+ helpers ERB::Util
11
+ helpers Web::FilteringHelpers
12
+ helpers Web::WorldHelpers
13
+ helpers Web::ConsoleHelpers
14
+
15
+ get('/') do
16
+ options = find_execution_plans_options
17
+ @plans = world.persistence.find_execution_plans(options)
18
+ erb :index
19
+ end
20
+
21
+ get('/:execution_plan_id/actions/:action_id/sub_plans') do |execution_plan_id, action_id|
22
+ options = find_execution_plans_options(true)
23
+ options[:filters].update('caller_execution_plan_id' => execution_plan_id,
24
+ 'caller_action_id' => action_id)
25
+ @plans = world.persistence.find_execution_plans(options)
26
+ erb :index
27
+ end
28
+
29
+ get('/status') do
30
+ # TODO: create a separate page for the overall status, linking
31
+ # to the more detailed pages
32
+ redirect to '/worlds'
33
+ end
34
+
35
+ get('/worlds') do
36
+ @worlds = world.coordinator.find_worlds
37
+ erb :worlds
38
+ end
39
+
40
+ post('/worlds/:id/ping') do |id|
41
+ timeout = 5
42
+ ping_response = world.ping(id, timeout).wait
43
+ if ping_response.failed?
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}")
50
+ end
51
+
52
+ post('/worlds/:id/invalidate') do |id|
53
+ invalidated_world = world.coordinator.find_worlds(false, id: id).first
54
+ unless invalidated_world
55
+ response = "World #{id} not found"
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)}")
65
+ end
66
+
67
+ get('/:id') do |id|
68
+ @plan = world.persistence.load_execution_plan(id)
69
+ erb :show
70
+ end
71
+
72
+ post('/:id/resume') do |id|
73
+ plan = world.persistence.load_execution_plan(id)
74
+ if plan.state != :paused
75
+ redirect(url "/#{plan.id}?notice=#{url_encode('The exeuction has to be paused to be able to resume')}")
76
+ else
77
+ world.execute(plan.id)
78
+ redirect(url "/#{plan.id}?notice=#{url_encode('The execution was resumed')}")
79
+ end
80
+ end
81
+
82
+ post('/:id/skip/:step_id') do |id, step_id|
83
+ plan = world.persistence.load_execution_plan(id)
84
+ step = plan.steps[step_id.to_i]
85
+ if plan.state != :paused
86
+ redirect(url "/#{plan.id}?notice=#{url_encode('The exeuction has to be paused to be able to skip')}")
87
+ elsif step.state != :error
88
+ redirect(url "/#{plan.id}?notice=#{url_encode('The step has to be failed to be able to skip')}")
89
+ else
90
+ plan.skip(step)
91
+ redirect(url "/#{plan.id}")
92
+ end
93
+ end
94
+
95
+ post('/:id/cancel/:step_id') do |id, step_id|
96
+ plan = world.persistence.load_execution_plan(id)
97
+ step = plan.steps[step_id.to_i]
98
+ if step.cancellable?
99
+ world.event(plan.id, step.id, Dynflow::Action::Cancellable::Cancel)
100
+ redirect(url "/#{plan.id}?notice=#{url_encode('The step was asked to cancel')}")
101
+ else
102
+ redirect(url "/#{plan.id}?notice=#{url_encode('The step does not support cancelling')}")
103
+ end
104
+ end
105
+
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,158 @@
1
+ module Dynflow
2
+ module Web
3
+ module ConsoleHelpers
4
+ def prettify_value(value)
5
+ YAML.dump(value)
6
+ end
7
+
8
+ def prettyprint(value)
9
+ value = prettyprint_references(value)
10
+ if value
11
+ pretty_value = prettify_value(value)
12
+ <<-HTML
13
+ <pre class="prettyprint lang-yaml">#{h(pretty_value)}</pre>
14
+ HTML
15
+ else
16
+ ""
17
+ end
18
+ end
19
+
20
+ def prettyprint_references(value)
21
+ case value
22
+ when Hash
23
+ value.reduce({}) do |h, (key, val)|
24
+ h.update(key => prettyprint_references(val))
25
+ end
26
+ when Array
27
+ value.map { |val| prettyprint_references(val) }
28
+ when ExecutionPlan::OutputReference
29
+ value.inspect
30
+ else
31
+ value
32
+ end
33
+ end
34
+
35
+ def duration_to_s(duration)
36
+ h("%0.2fs" % duration)
37
+ end
38
+
39
+ def load_action(step)
40
+ world.persistence.load_action(step)
41
+ end
42
+
43
+ def step_error(step)
44
+ if step.error
45
+ ['<pre>',
46
+ "#{h(step.error.message)} (#{h(step.error.exception_class)})\n",
47
+ h(step.error.backtrace.join("\n")),
48
+ '</pre>'].join
49
+ end
50
+ end
51
+
52
+ def show_world(world_id)
53
+ if registered_world = world.coordinator.find_worlds(false, id: world_id).first
54
+ "%{world_id} %{world_meta}" % { world_id: world_id, world_meta: registered_world.meta.inspect }
55
+ else
56
+ world_id
57
+ end
58
+ end
59
+
60
+ def show_action_data(label, value)
61
+ value_html = prettyprint(value)
62
+ if !value_html.empty?
63
+ <<-HTML
64
+ <p>
65
+ <b>#{h(label)}</b>
66
+ #{value_html}
67
+ </p>
68
+ HTML
69
+ else
70
+ ""
71
+ end
72
+ end
73
+
74
+ def atom_css_classes(atom)
75
+ classes = ["atom"]
76
+ step = @plan.steps[atom.step_id]
77
+ case step.state
78
+ when :success
79
+ classes << "success"
80
+ when :error
81
+ classes << "error"
82
+ when :skipped, :skipping
83
+ classes << "skipped"
84
+ end
85
+ return classes.join(" ")
86
+ end
87
+
88
+ def flow_css_classes(flow, sub_flow = nil)
89
+ classes = []
90
+ case flow
91
+ when Flows::Sequence
92
+ classes << "sequence"
93
+ when Flows::Concurrence
94
+ classes << "concurrence"
95
+ when Flows::Atom
96
+ classes << atom_css_classes(flow)
97
+ else
98
+ raise "Unknown run plan #{run_plan.inspect}"
99
+ end
100
+ classes << atom_css_classes(sub_flow) if sub_flow.is_a? Flows::Atom
101
+ return classes.join(" ")
102
+ end
103
+
104
+ def step_css_class(step)
105
+ case step.state
106
+ when :success
107
+ "success"
108
+ when :error
109
+ "important"
110
+ end
111
+ end
112
+
113
+ def progress_width(step)
114
+ if step.state == :error
115
+ 100 # we want to show the red bar in full width
116
+ else
117
+ step.progress_done * 100
118
+ end
119
+ end
120
+
121
+ def step(step_id)
122
+ @plan.steps[step_id]
123
+ end
124
+
125
+ def updated_url(new_params)
126
+ url(request.path_info + "?" + Rack::Utils.build_nested_query(params.merge(new_params.stringify_keys)))
127
+ end
128
+
129
+ def paginated_url(delta)
130
+ h(updated_url(page: [0, page + delta].max.to_s))
131
+ end
132
+
133
+ def order_link(attr, label)
134
+ return h(label) unless supported_ordering?(attr)
135
+ new_ordering_options = { order_by: attr.to_s,
136
+ desc: false }
137
+ arrow = ""
138
+ if ordering_options[:order_by].to_s == attr.to_s
139
+ arrow = ordering_options[:desc] ? "&#9660;" : "&#9650;"
140
+ new_ordering_options[:desc] = !ordering_options[:desc]
141
+ end
142
+ url = updated_url(new_ordering_options)
143
+ return %{<a href="#{url}"> #{arrow} #{h(label)}</a>}
144
+ end
145
+
146
+ def filter_checkbox(field, values)
147
+ out = "<p>#{field}: %s</p>"
148
+ checkboxes = values.map do |value|
149
+ field_filter = filtering_options[:filters][field]
150
+ checked = field_filter && field_filter.include?(value)
151
+ %{<input type="checkbox" name="filters[#{field}][]" value="#{value}" #{ "checked" if checked }/>#{value}}
152
+ end.join(' ')
153
+ out %= checkboxes
154
+ return out
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,85 @@
1
+ module Dynflow
2
+ module Web
3
+ module FilteringHelpers
4
+ def supported_filter?(filter_attr)
5
+ world.persistence.adapter.filtering_by.any? do |attr|
6
+ attr.to_s == filter_attr.to_s
7
+ end
8
+ end
9
+
10
+ def filtering_options(show_all = false)
11
+ return @filtering_options if @filtering_options
12
+
13
+ if params[:filters]
14
+ params[:filters].map do |key, value|
15
+ unless supported_filter?(key)
16
+ halt 400, "Unsupported ordering"
17
+ end
18
+ end
19
+
20
+ filters = params[:filters]
21
+ elsif supported_filter?('state')
22
+ excluded_states = show_all ? [] : ['stopped']
23
+ filters = { 'state' => ExecutionPlan.states.map(&:to_s) - excluded_states }
24
+ else
25
+ filters = {}
26
+ end
27
+ @filtering_options = { filters: filters }.with_indifferent_access
28
+ return @filtering_options
29
+ end
30
+
31
+ def find_execution_plans_options(show_all = false)
32
+ options = HashWithIndifferentAccess.new
33
+ options.merge!(filtering_options(show_all))
34
+ options.merge!(pagination_options)
35
+ options.merge!(ordering_options)
36
+ end
37
+
38
+ def paginate?
39
+ world.persistence.adapter.pagination?
40
+ end
41
+
42
+ def page
43
+ (params[:page] || 0).to_i
44
+ end
45
+
46
+ def per_page
47
+ (params[:per_page] || 10).to_i
48
+ end
49
+
50
+ def pagination_options
51
+ if paginate?
52
+ { page: page, per_page: per_page }
53
+ else
54
+ if params[:page] || params[:per_page]
55
+ halt 400, "The persistence doesn't support pagination"
56
+ end
57
+ return {}
58
+ end
59
+ end
60
+
61
+ def supported_ordering?(ord_attr)
62
+ world.persistence.adapter.ordering_by.any? do |attr|
63
+ attr.to_s == ord_attr.to_s
64
+ end
65
+ end
66
+
67
+ def ordering_options
68
+ return @ordering_options if @ordering_options
69
+
70
+ if params[:order_by]
71
+ unless supported_ordering?(params[:order_by])
72
+ halt 400, "Unsupported ordering"
73
+ end
74
+ @ordering_options = { order_by: params[:order_by],
75
+ desc: (params[:desc] == 'true') }
76
+ elsif supported_ordering?('started_at')
77
+ @ordering_options = { order_by: 'started_at', desc: true }
78
+ else
79
+ @ordering_options = {}
80
+ end
81
+ return @ordering_options
82
+ end
83
+ end
84
+ end
85
+ end