dynflow 0.1.0 → 0.2.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 (133) hide show
  1. data/.gitignore +6 -0
  2. data/.travis.yml +9 -0
  3. data/Gemfile +0 -10
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +99 -37
  6. data/Rakefile +2 -6
  7. data/doc/images/logo.png +0 -0
  8. data/dynflow.gemspec +10 -1
  9. data/examples/generate_work_for_daemon.rb +24 -0
  10. data/examples/orchestrate.rb +121 -0
  11. data/examples/run_daemon.rb +17 -0
  12. data/examples/web_console.rb +29 -0
  13. data/lib/dynflow.rb +27 -6
  14. data/lib/dynflow/action.rb +185 -77
  15. data/lib/dynflow/action/cancellable_polling.rb +18 -0
  16. data/lib/dynflow/action/finalize_phase.rb +18 -0
  17. data/lib/dynflow/action/flow_phase.rb +44 -0
  18. data/lib/dynflow/action/format.rb +46 -0
  19. data/lib/dynflow/action/missing.rb +26 -0
  20. data/lib/dynflow/action/plan_phase.rb +85 -0
  21. data/lib/dynflow/action/polling.rb +49 -0
  22. data/lib/dynflow/action/presenter.rb +51 -0
  23. data/lib/dynflow/action/progress.rb +62 -0
  24. data/lib/dynflow/action/run_phase.rb +43 -0
  25. data/lib/dynflow/action/suspended.rb +21 -0
  26. data/lib/dynflow/clock.rb +133 -0
  27. data/lib/dynflow/daemon.rb +29 -0
  28. data/lib/dynflow/execution_plan.rb +285 -33
  29. data/lib/dynflow/execution_plan/dependency_graph.rb +29 -0
  30. data/lib/dynflow/execution_plan/output_reference.rb +52 -0
  31. data/lib/dynflow/execution_plan/steps.rb +12 -0
  32. data/lib/dynflow/execution_plan/steps/abstract.rb +121 -0
  33. data/lib/dynflow/execution_plan/steps/abstract_flow_step.rb +52 -0
  34. data/lib/dynflow/execution_plan/steps/error.rb +33 -0
  35. data/lib/dynflow/execution_plan/steps/finalize_step.rb +23 -0
  36. data/lib/dynflow/execution_plan/steps/plan_step.rb +81 -0
  37. data/lib/dynflow/execution_plan/steps/run_step.rb +21 -0
  38. data/lib/dynflow/executors.rb +9 -0
  39. data/lib/dynflow/executors/abstract.rb +32 -0
  40. data/lib/dynflow/executors/parallel.rb +88 -0
  41. data/lib/dynflow/executors/parallel/core.rb +119 -0
  42. data/lib/dynflow/executors/parallel/execution_plan_manager.rb +120 -0
  43. data/lib/dynflow/executors/parallel/flow_manager.rb +48 -0
  44. data/lib/dynflow/executors/parallel/pool.rb +102 -0
  45. data/lib/dynflow/executors/parallel/running_steps_manager.rb +63 -0
  46. data/lib/dynflow/executors/parallel/sequence_cursor.rb +97 -0
  47. data/lib/dynflow/executors/parallel/sequential_manager.rb +81 -0
  48. data/lib/dynflow/executors/parallel/work_queue.rb +44 -0
  49. data/lib/dynflow/executors/parallel/worker.rb +30 -0
  50. data/lib/dynflow/executors/remote_via_socket.rb +38 -0
  51. data/lib/dynflow/executors/remote_via_socket/core.rb +150 -0
  52. data/lib/dynflow/flows.rb +13 -0
  53. data/lib/dynflow/flows/abstract.rb +36 -0
  54. data/lib/dynflow/flows/abstract_composed.rb +104 -0
  55. data/lib/dynflow/flows/atom.rb +36 -0
  56. data/lib/dynflow/flows/concurrence.rb +28 -0
  57. data/lib/dynflow/flows/sequence.rb +13 -0
  58. data/lib/dynflow/future.rb +173 -0
  59. data/lib/dynflow/listeners.rb +7 -0
  60. data/lib/dynflow/listeners/abstract.rb +13 -0
  61. data/lib/dynflow/listeners/serialization.rb +41 -0
  62. data/lib/dynflow/listeners/socket.rb +88 -0
  63. data/lib/dynflow/logger_adapters.rb +8 -0
  64. data/lib/dynflow/logger_adapters/abstract.rb +30 -0
  65. data/lib/dynflow/logger_adapters/delegator.rb +13 -0
  66. data/lib/dynflow/logger_adapters/formatters.rb +8 -0
  67. data/lib/dynflow/logger_adapters/formatters/abstract.rb +33 -0
  68. data/lib/dynflow/logger_adapters/formatters/exception.rb +15 -0
  69. data/lib/dynflow/logger_adapters/simple.rb +59 -0
  70. data/lib/dynflow/micro_actor.rb +102 -0
  71. data/lib/dynflow/persistence.rb +53 -0
  72. data/lib/dynflow/persistence_adapters.rb +6 -0
  73. data/lib/dynflow/persistence_adapters/abstract.rb +56 -0
  74. data/lib/dynflow/persistence_adapters/sequel.rb +160 -0
  75. data/lib/dynflow/persistence_adapters/sequel_migrations/001_initial.rb +52 -0
  76. data/lib/dynflow/serializable.rb +66 -0
  77. data/lib/dynflow/simple_world.rb +18 -0
  78. data/lib/dynflow/stateful.rb +40 -0
  79. data/lib/dynflow/testing.rb +32 -0
  80. data/lib/dynflow/testing/assertions.rb +64 -0
  81. data/lib/dynflow/testing/dummy_execution_plan.rb +40 -0
  82. data/lib/dynflow/testing/dummy_executor.rb +29 -0
  83. data/lib/dynflow/testing/dummy_planned_action.rb +18 -0
  84. data/lib/dynflow/testing/dummy_step.rb +19 -0
  85. data/lib/dynflow/testing/dummy_world.rb +33 -0
  86. data/lib/dynflow/testing/factories.rb +83 -0
  87. data/lib/dynflow/testing/managed_clock.rb +23 -0
  88. data/lib/dynflow/testing/mimic.rb +38 -0
  89. data/lib/dynflow/transaction_adapters.rb +9 -0
  90. data/lib/dynflow/transaction_adapters/abstract.rb +26 -0
  91. data/lib/dynflow/transaction_adapters/active_record.rb +27 -0
  92. data/lib/dynflow/transaction_adapters/none.rb +12 -0
  93. data/lib/dynflow/version.rb +1 -1
  94. data/lib/dynflow/web_console.rb +277 -0
  95. data/lib/dynflow/world.rb +168 -0
  96. data/test/action_test.rb +89 -11
  97. data/test/clock_test.rb +59 -0
  98. data/test/code_workflow_example.rb +382 -0
  99. data/test/execution_plan_test.rb +195 -64
  100. data/test/executor_test.rb +692 -0
  101. data/test/persistance_adapters_test.rb +173 -0
  102. data/test/test_helper.rb +316 -1
  103. data/test/testing_test.rb +148 -0
  104. data/test/web_console_test.rb +38 -0
  105. data/web/assets/javascripts/application.js +25 -0
  106. data/web/assets/stylesheets/application.css +101 -0
  107. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.css +1109 -0
  108. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.min.css +9 -0
  109. data/web/assets/vendor/bootstrap/css/bootstrap.css +6167 -0
  110. data/web/assets/vendor/bootstrap/css/bootstrap.min.css +9 -0
  111. data/web/assets/vendor/bootstrap/img/glyphicons-halflings-white.png +0 -0
  112. data/web/assets/vendor/bootstrap/img/glyphicons-halflings.png +0 -0
  113. data/web/assets/vendor/bootstrap/js/bootstrap.js +2280 -0
  114. data/web/assets/vendor/bootstrap/js/bootstrap.min.js +6 -0
  115. data/web/assets/vendor/google-code-prettify/lang-basic.js +3 -0
  116. data/web/assets/vendor/google-code-prettify/prettify.css +1 -0
  117. data/web/assets/vendor/google-code-prettify/prettify.js +30 -0
  118. data/web/assets/vendor/google-code-prettify/run_prettify.js +34 -0
  119. data/web/assets/vendor/jquery/jquery.js +9807 -0
  120. data/web/views/flow.erb +19 -0
  121. data/web/views/flow_step.erb +31 -0
  122. data/web/views/index.erb +39 -0
  123. data/web/views/layout.erb +20 -0
  124. data/web/views/plan_step.erb +11 -0
  125. data/web/views/show.erb +54 -0
  126. metadata +250 -11
  127. data/examples/events.rb +0 -71
  128. data/examples/workflow.rb +0 -140
  129. data/lib/dynflow/bus.rb +0 -168
  130. data/lib/dynflow/dispatcher.rb +0 -36
  131. data/lib/dynflow/logger.rb +0 -34
  132. data/lib/dynflow/step.rb +0 -234
  133. data/test/bus_test.rb +0 -150
@@ -0,0 +1,29 @@
1
+ # Demo for Dynflow web console
2
+ # usage: ruby web_console.rb
3
+
4
+ $:.unshift(File.expand_path('../../lib', __FILE__))
5
+
6
+ require 'dynflow'
7
+ require_relative 'orchestrate'
8
+
9
+ world = Dynflow::SimpleWorld.new
10
+
11
+ require 'dynflow/web_console'
12
+ dynflow_console = Dynflow::WebConsole.setup do
13
+ set :world, world
14
+ end
15
+
16
+ 3.times do
17
+ Thread.new do
18
+ 3.times do
19
+ world.trigger(Orchestrate::CreateInfrastructure)
20
+ end
21
+ end
22
+ end
23
+
24
+ puts <<MESSAGE
25
+ =============================================
26
+ See the console at http://localhost:4567/
27
+ =============================================
28
+ MESSAGE
29
+ dynflow_console.run!
data/lib/dynflow.rb CHANGED
@@ -1,11 +1,32 @@
1
+ require 'apipie-params'
2
+ require 'algebrick'
3
+ require 'thread'
4
+ require 'set'
1
5
  require 'active_support/core_ext/hash/indifferent_access'
2
- require 'dynflow/logger'
3
- require 'dynflow/execution_plan'
4
- require 'dynflow/dispatcher'
5
- require 'dynflow/bus'
6
- require 'dynflow/step'
7
- require 'dynflow/action'
8
6
 
7
+ # TODO validate in/output, also validate unknown keys
8
+ # TODO performance testing, how many actions will it handle?
9
+ # TODO profiling, find bottlenecks
9
10
  module Dynflow
10
11
 
12
+ class Error < StandardError
13
+ end
14
+
15
+ require 'dynflow/future'
16
+ require 'dynflow/micro_actor'
17
+ require 'dynflow/serializable'
18
+ require 'dynflow/clock'
19
+ require 'dynflow/stateful'
20
+ require 'dynflow/transaction_adapters'
21
+ require 'dynflow/persistence'
22
+ require 'dynflow/action'
23
+ require 'dynflow/flows'
24
+ require 'dynflow/execution_plan'
25
+ require 'dynflow/listeners'
26
+ require 'dynflow/executors'
27
+ require 'dynflow/logger_adapters'
28
+ require 'dynflow/world'
29
+ require 'dynflow/simple_world'
30
+ require 'dynflow/daemon'
31
+
11
32
  end
@@ -1,130 +1,238 @@
1
+ require 'active_support/inflector'
2
+
1
3
  module Dynflow
2
- class Action
3
4
 
4
- # only for the planning phase: flag indicating that the action
5
- # was triggered from subscription. If so, the implicit plan
6
- # method uses the input of the parent action. Otherwise, the
7
- # argument the plan_action is used as default.
8
- attr_accessor :execution_plan, :from_subscription, :input, :output
5
+ # TODO unify phases into one class, check what can be called in what phase at runtime
6
+ class Action < Serializable
7
+ include Algebrick::TypeCheck
8
+ include Algebrick::Matching
9
9
 
10
- def self.inherited(child)
11
- self.actions << child
10
+ require 'dynflow/action/format'
11
+ extend Format
12
+
13
+ require 'dynflow/action/progress'
14
+ include Progress
15
+
16
+ require 'dynflow/action/suspended'
17
+ require 'dynflow/action/missing'
18
+
19
+ require 'dynflow/action/plan_phase'
20
+ require 'dynflow/action/flow_phase'
21
+ require 'dynflow/action/run_phase'
22
+ require 'dynflow/action/finalize_phase'
23
+
24
+ require 'dynflow/action/presenter'
25
+ require 'dynflow/action/polling'
26
+ require 'dynflow/action/cancellable_polling'
27
+
28
+ # Override this to extend the phase classes
29
+ def self.phase_modules
30
+ { plan_phase: [PlanPhase],
31
+ run_phase: [RunPhase],
32
+ finalize_phase: [FinalizePhase],
33
+ presenter: [Presenter] }.freeze
34
+ end
35
+
36
+ phase_modules.each do |phase_name, _|
37
+ define_singleton_method phase_name do
38
+ instance_variable_get :"@#{phase_name}" or
39
+ instance_variable_set :"@#{phase_name}", __send__("create_#{phase_name}")
40
+ end
41
+
42
+ define_singleton_method "create_#{phase_name}" do
43
+ generate_phase(*phase_modules[phase_name])
44
+ end
12
45
  end
13
46
 
14
- def self.actions
15
- @actions ||= []
47
+ def self.generate_phase(*modules)
48
+ Class.new(self) { modules.each { |m| include m } }
16
49
  end
17
50
 
51
+ def self.phase?
52
+ [PlanPhase, RunPhase, FinalizePhase, Presenter].any? { |phase| self < phase }
53
+ end
54
+
55
+ def self.all_children
56
+ #noinspection RubyArgCount
57
+ children.
58
+ inject(children) { |children, child| children + child.all_children }.
59
+ select { |ch| !ch.phase? }
60
+ end
61
+
62
+ # FIND define subscriptions in world independent on action's classes,
63
+ # limited only by in/output formats
64
+ # @return [nil, Class] a child of Action
18
65
  def self.subscribe
19
66
  nil
20
67
  end
21
68
 
22
- def self.require
23
- nil
69
+ def self.attr_indifferent_access_hash(*names)
70
+ attr_reader(*names)
71
+ names.each do |name|
72
+ define_method("#{name}=") { |v| indifferent_access_hash_variable_set name, v }
73
+ end
24
74
  end
25
75
 
26
- def initialize(input, output = nil)
27
- # for preparation phase
28
- @execution_plan = ExecutionPlan.new
76
+ def indifferent_access_hash_variable_set(name, value)
77
+ Type! value, Hash
78
+ instance_variable_set :"@#{name}", value.with_indifferent_access
79
+ end
29
80
 
30
- @input = input
31
- @output = output || {}
81
+ def self.from_hash(hash, phase, *args)
82
+ check_class_key_present hash
83
+ raise ArgumentError, "unknown phase '#{phase}'" unless [:plan_phase, :run_phase, :finalize_phase].include? phase
84
+ Action.constantize(hash[:class]).send(phase).new_from_hash(hash, *args)
32
85
  end
33
86
 
87
+ attr_reader :world, :execution_plan_id, :id, :plan_step_id, :run_step_id, :finalize_step_id
34
88
 
35
- def ==(other)
36
- [self.class.name, self.input, self.output] ==
37
- [other.class.name, other.input, other.output]
38
- end
89
+ def initialize(attributes, world)
90
+ raise "It's not expected to initialize this class directly, use phases." unless self.class.phase?
91
+
92
+ Type! attributes, Hash
39
93
 
40
- def inspect
41
- "#{self.class.name}: #{input.inspect} ~> #{output.inspect}"
94
+ @world = Type! world, World
95
+ @step = Type! attributes[:step], ExecutionPlan::Steps::Abstract
96
+ @execution_plan_id = attributes[:execution_plan_id] || raise(ArgumentError, 'missing execution_plan_id')
97
+ @id = attributes[:id] || raise(ArgumentError, 'missing id')
98
+ @plan_step_id = attributes[:plan_step_id]
99
+ @run_step_id = attributes[:run_step_id]
100
+ @finalize_step_id = attributes[:finalize_step_id]
42
101
  end
43
102
 
44
- # the block contains the expression in Apipie::Params::DSL
45
- # describing the format of message
46
- def self.input_format(&block)
47
- if block
48
- @input_format_block = block
49
- elsif @input_format_block
50
- @input_format ||= Apipie::Params::Description.define(&@input_format_block)
103
+ def self.action_class
104
+ # superclass because we run this from the phases of action class
105
+ if phase?
106
+ superclass
51
107
  else
52
- nil
108
+ self
53
109
  end
54
110
  end
55
111
 
56
- # the block contains the expression in Apipie::Params::DSL
57
- # describing the format of message
58
- def self.output_format(&block)
59
- if block
60
- @output_format_block = block
61
- elsif @output_format_block
62
- @output_format ||= Apipie::Params::Description.define(&@output_format_block)
63
- else
64
- nil
65
- end
112
+ def self.constantize(action_name)
113
+ action_name.constantize
114
+ rescue NameError
115
+ Action::Missing.generate(action_name)
66
116
  end
67
117
 
68
- def self.trigger(*args)
69
- Dynflow::Bus.trigger(self, *args)
118
+ def action_logger
119
+ world.action_logger
70
120
  end
71
121
 
72
- def self.plan(*args)
73
- action = self.new({})
74
- yield action if block_given?
122
+ def action_class
123
+ self.class.action_class
124
+ end
75
125
 
76
- plan_step = Step::Plan.new(action)
77
- action.execution_plan.plan_steps << plan_step
78
- plan_step.catch_errors do
79
- action.plan(*args)
80
- end
126
+ def to_hash
127
+ recursive_to_hash class: action_class.name,
128
+ execution_plan_id: execution_plan_id,
129
+ id: id,
130
+ plan_step_id: plan_step_id,
131
+ run_step_id: run_step_id,
132
+ finalize_step_id: finalize_step_id
133
+ end
81
134
 
82
- if action.execution_plan.failed_steps.any?
83
- action.execution_plan.status = 'error'
84
- else
85
- action.add_subscriptions(*args)
86
- end
135
+ # @api private
136
+ # @return [Array<Fixnum>] - ids of steps referenced from action
137
+ def required_step_ids(value = self.input)
138
+ ret = case value
139
+ when Hash
140
+ value.values.map { |val| required_step_ids(val) }
141
+ when Array
142
+ value.map { |val| required_step_ids(val) }
143
+ when ExecutionPlan::OutputReference
144
+ value.step_id
145
+ else
146
+ # no reference hidden in this arg
147
+ end
148
+ return Array(ret).flatten.compact
149
+ end
87
150
 
88
- action.execution_plan
151
+ def state
152
+ @step.state
89
153
  end
90
154
 
91
- # for subscribed actions: by default take the input of the
92
- # subscribed action
155
+ def error
156
+ @step.error
157
+ end
158
+
159
+ protected
160
+
161
+ def state=(state)
162
+ @world.logger.debug "step #{execution_plan_id}:#{@step.id} #{self.state} >> #{state}"
163
+ @step.state = state
164
+ end
165
+
166
+ def save_state
167
+ @step.save
168
+ end
169
+
170
+ # @override
93
171
  def plan(*args)
94
- if from_subscription
172
+ if trigger
95
173
  # if the action is triggered by subscription, by default use the
96
- # input of parent action
97
- plan_self(self.input)
174
+ # input of parent action.
175
+ # should be replaced by referencing the input from input format
176
+ plan_self(input.merge(trigger.input))
98
177
  else
99
178
  # in this case, the action was triggered by plan_action. Use
100
179
  # the argument specified there.
101
- plan_self(args.first)
180
+ plan_self(*args)
102
181
  end
182
+ self
103
183
  end
104
184
 
105
- def plan_self(input)
106
- self.input = input
107
- @execution_plan << self
185
+ def self.new_from_hash(hash, world)
186
+ new(hash, world)
108
187
  end
109
188
 
110
- def plan_action(action_class, *args)
111
- sub_action_plan = action_class.plan(*args) do |action|
112
- action.input = self.input
113
- end
114
- @execution_plan.concat(sub_action_plan)
189
+ private
190
+
191
+ ERRORING = Object.new
192
+
193
+ # DSL to terminate action execution and set it to error
194
+ def error!(error)
195
+ set_error(error)
196
+ throw ERRORING
115
197
  end
116
198
 
117
- def add_subscriptions(*plan_args)
118
- @execution_plan.concat(Dispatcher.execution_plan_for(self, *plan_args))
199
+ def with_error_handling(&block)
200
+ raise "wrong state #{self.state}" unless self.state == :running
201
+
202
+ begin
203
+ catch(ERRORING) { block.call }
204
+ rescue Exception => error
205
+ set_error(error)
206
+ # reraise low-level exceptions
207
+ raise error unless Type? error, StandardError, ScriptError
208
+ end
209
+
210
+ case self.state
211
+ when :running
212
+ self.state = :success
213
+ when :suspended, :error
214
+ else
215
+ raise "wrong state #{self.state}"
216
+ end
119
217
  end
120
218
 
121
- def execution_plan
122
- @execution_plan
219
+ def set_error(error)
220
+ Type! error, Exception, String
221
+ action_logger.error error
222
+ self.state = :error
223
+ @step.error = if error.is_a?(String)
224
+ ExecutionPlan::Steps::Error.new(nil, error, nil)
225
+ else
226
+ ExecutionPlan::Steps::Error.new(error.class.name, error.message, error.backtrace)
227
+ end
123
228
  end
124
229
 
125
- def validate!
126
- self.clss.output_format.validate!(output)
230
+ def self.inherited(child)
231
+ children << child
127
232
  end
128
233
 
234
+ def self.children
235
+ @children ||= []
236
+ end
129
237
  end
130
238
  end
@@ -0,0 +1,18 @@
1
+ module Dynflow
2
+ module Action::CancellablePolling
3
+ include Action::Polling
4
+ Cancel = Algebrick.atom
5
+
6
+ def run(event = nil)
7
+ if Cancel === event
8
+ self.external_task = cancel_external_task
9
+ else
10
+ super event
11
+ end
12
+ end
13
+
14
+ def cancel_external_task
15
+ NotImplementedError
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ module Dynflow
2
+ module Action::FinalizePhase
3
+
4
+ def self.included(base)
5
+ base.send(:include, Action::FlowPhase)
6
+ base.send(:attr_reader, :output)
7
+ end
8
+
9
+ def execute
10
+ self.state = :running
11
+ save_state
12
+ with_error_handling do
13
+ finalize
14
+ end
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,44 @@
1
+ module Dynflow
2
+ module Action::FlowPhase
3
+
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ base.send(:attr_reader, :input)
7
+ end
8
+
9
+ def initialize(attributes, world)
10
+ super attributes, world
11
+
12
+ indifferent_access_hash_variable_set :input, deserialize_references(attributes[:input])
13
+ indifferent_access_hash_variable_set :output, attributes[:output] || {}
14
+ end
15
+
16
+ def to_hash
17
+ super.merge recursive_to_hash(input: input,
18
+ output: output)
19
+ end
20
+
21
+ def deserialize_references(value)
22
+ case value
23
+ when Hash
24
+ if value[:class] == "Dynflow::ExecutionPlan::OutputReference"
25
+ ExecutionPlan::OutputReference.new_from_hash(value)
26
+ else
27
+ value.reduce(HashWithIndifferentAccess.new) do |h, (key, val)|
28
+ h.update(key => deserialize_references(val))
29
+ end
30
+ end
31
+ when Array
32
+ value.map { |val| deserialize_references(val) }
33
+ else
34
+ value
35
+ end
36
+ end
37
+
38
+ module ClassMethods
39
+ def new_from_hash(hash, step, world)
40
+ new(hash.merge(step: step), world)
41
+ end
42
+ end
43
+ end
44
+ end