dynflow 0.6.2 → 0.7.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.
- data/README.md +34 -14
- data/doc/images/screenshot.png +0 -0
- data/examples/example_helper.rb +47 -0
- data/examples/orchestrate.rb +58 -22
- data/examples/orchestrate_evented.rb +174 -0
- data/examples/remote_executor.rb +76 -0
- data/lib/dynflow.rb +1 -0
- data/lib/dynflow/action.rb +29 -13
- data/lib/dynflow/action/{cancellable_polling.rb → cancellable.rb} +3 -4
- data/lib/dynflow/action/rescue.rb +59 -0
- data/lib/dynflow/errors.rb +28 -0
- data/lib/dynflow/execution_plan.rb +41 -10
- data/lib/dynflow/execution_plan/steps/abstract.rb +14 -3
- data/lib/dynflow/execution_plan/steps/error.rb +6 -1
- data/lib/dynflow/execution_plan/steps/finalize_step.rb +5 -0
- data/lib/dynflow/execution_plan/steps/run_step.rb +20 -2
- data/lib/dynflow/executors/parallel/core.rb +31 -3
- data/lib/dynflow/version.rb +1 -1
- data/lib/dynflow/web_console.rb +13 -1
- data/lib/dynflow/world.rb +4 -2
- data/test/execution_plan_test.rb +15 -2
- data/test/executor_test.rb +1 -1
- data/test/persistance_adapters_test.rb +1 -1
- data/test/rescue_test.rb +164 -0
- data/test/support/code_workflow_example.rb +5 -4
- data/test/support/rescue_example.rb +73 -0
- data/test/test_helper.rb +6 -3
- data/web/assets/stylesheets/application.css +4 -0
- data/web/views/flow_step.erb +5 -1
- data/web/views/show.erb +3 -1
- metadata +13 -6
- data/examples/generate_work_for_daemon.rb +0 -24
- data/examples/run_daemon.rb +0 -17
- data/examples/web_console.rb +0 -29
data/README.md
CHANGED
@@ -12,6 +12,9 @@ written in Ruby that allows to:
|
|
12
12
|
* extend the workflows from third-party libraries
|
13
13
|
* keep consistency between local transactional database and
|
14
14
|
external services
|
15
|
+
* suspend the long-running steps, not blocking the thread pool
|
16
|
+
* cancel steps when possible
|
17
|
+
* extend the actions behavior with middlewares
|
15
18
|
* define the input/output interface between the building blocks (planned)
|
16
19
|
* define rollback for the workflow (planned)
|
17
20
|
* have multiple workers for distributing the load (planned)
|
@@ -22,8 +25,12 @@ persistence, transaction layer or executor implementation, giving you
|
|
22
25
|
the last word in choosing the right one (providing default
|
23
26
|
implementations as well).
|
24
27
|
|
28
|
+

|
29
|
+
|
25
30
|
* [Current status](#current-status)
|
26
31
|
* [How it works](#how-it-works)
|
32
|
+
* [Examples](#examples)
|
33
|
+
* [The Anatomy of Action Class](#the-anatomy-of-action-class)
|
27
34
|
* [Glossary](#glossary)
|
28
35
|
* [Related projects](#related-projects)
|
29
36
|
|
@@ -35,12 +42,6 @@ to support the services orchestration in the
|
|
35
42
|
[Katello](http://katello.org) and [Foreman](http://theforeman.org/)
|
36
43
|
projects, getting to production-ready state in couple of weeks.
|
37
44
|
|
38
|
-
Requirements
|
39
|
-
------------
|
40
|
-
|
41
|
-
- Ruby MRI 1.9.3, 2.0, or 2.1.
|
42
|
-
- It does not work on JRuby nor Rubinius yet.
|
43
|
-
|
44
45
|
How it works
|
45
46
|
------------
|
46
47
|
|
@@ -90,14 +91,28 @@ The output of this phase is a set of actions and their inputs.
|
|
90
91
|
|
91
92
|
Every action can participate in every phase.
|
92
93
|
|
93
|
-
|
94
|
-
|
94
|
+
Examples
|
95
|
+
--------
|
95
96
|
|
96
|
-
|
97
|
+
The `examples` directory contains simple ruby scripts different
|
98
|
+
features in action. You can just run the example files and see the Dynflow
|
99
|
+
in action.
|
100
|
+
|
101
|
+
* `orchestrate.rb` - example worlflow of getting some infrastructure
|
102
|
+
up and running, with ability to rescue from some error states.
|
103
|
+
|
104
|
+
* `orchestrate_evented.rb` - the same workflow using the ability to
|
105
|
+
suspend/wakeup actions while waiting for some external event.
|
106
|
+
It also demonstrates the ability to cancel actions that support it.
|
107
|
+
|
108
|
+
* `remote_executor.rb` - example of executing the flows in external
|
109
|
+
process
|
97
110
|
|
98
|
-
```ruby
|
99
|
-
# The anatomy of action class
|
100
111
|
|
112
|
+
The Anatomy of Action Class
|
113
|
+
---------------------------
|
114
|
+
|
115
|
+
```ruby
|
101
116
|
# every action needs to inherit from Dynflow::Action
|
102
117
|
class Action < Dynflow::Action
|
103
118
|
|
@@ -151,7 +166,6 @@ class Action < Dynflow::Action
|
|
151
166
|
end
|
152
167
|
end
|
153
168
|
```
|
154
|
-
|
155
169
|
Every action should be as atomic as possible, providing better
|
156
170
|
granularity when manipulating the process. Since every action can be
|
157
171
|
subscribed by another one, adding new behaviour to an existing
|
@@ -160,8 +174,6 @@ workflow is really simple.
|
|
160
174
|
The input and output format can be used for defining the interface
|
161
175
|
that other developers can use when extending the workflows.
|
162
176
|
|
163
|
-
See the examples directory for more complete examples.
|
164
|
-
|
165
177
|
Glossary
|
166
178
|
--------
|
167
179
|
|
@@ -208,6 +220,14 @@ Related projects
|
|
208
220
|
for running system tasks with Dynflow, comes with simple Web-UI for
|
209
221
|
testing it
|
210
222
|
|
223
|
+
|
224
|
+
Requirements
|
225
|
+
------------
|
226
|
+
|
227
|
+
- Ruby MRI 1.9.3, 2.0, or 2.1.
|
228
|
+
- It does not work on JRuby nor Rubinius yet.
|
229
|
+
|
230
|
+
|
211
231
|
License
|
212
232
|
-------
|
213
233
|
|
Binary file
|
@@ -0,0 +1,47 @@
|
|
1
|
+
$:.unshift(File.expand_path('../../lib', __FILE__))
|
2
|
+
|
3
|
+
require 'dynflow'
|
4
|
+
|
5
|
+
class ExampleHelper
|
6
|
+
class << self
|
7
|
+
def world
|
8
|
+
@world ||= create_world
|
9
|
+
end
|
10
|
+
|
11
|
+
def create_world(options = {})
|
12
|
+
options = default_world_options.merge(options)
|
13
|
+
Dynflow::SimpleWorld.new(options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def default_world_options
|
17
|
+
{ logger_adapter: logger_adapter }
|
18
|
+
end
|
19
|
+
|
20
|
+
def logger_adapter
|
21
|
+
Dynflow::LoggerAdapters::Simple.new $stderr, 4
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
def run_web_console(world = ExampleHelper.world)
|
26
|
+
require 'dynflow/web_console'
|
27
|
+
dynflow_console = Dynflow::WebConsole.setup do
|
28
|
+
set :world, world
|
29
|
+
end
|
30
|
+
dynflow_console.run!
|
31
|
+
end
|
32
|
+
|
33
|
+
# for simulation of the execution failing for the first time
|
34
|
+
def something_should_fail!
|
35
|
+
@should_fail = true
|
36
|
+
end
|
37
|
+
|
38
|
+
# for simulation of the execution failing for the first time
|
39
|
+
def something_should_fail?
|
40
|
+
@should_fail
|
41
|
+
end
|
42
|
+
|
43
|
+
def nothing_should_fail!
|
44
|
+
@should_fail = false
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/examples/orchestrate.rb
CHANGED
@@ -1,7 +1,30 @@
|
|
1
|
-
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
example_description = <<DESC
|
4
|
+
Orchestrate Example
|
5
|
+
===================
|
6
|
+
|
7
|
+
This example simulates a workflow of setting up an infrastructure, using
|
8
|
+
more high-level steps in CreateInfrastructure, that expand to smaller steps
|
9
|
+
of PrepareDisk, CraeteVM etc.
|
10
|
+
|
11
|
+
It shows the possibility to run the independend actions concurrently, chaining
|
12
|
+
the actions (passing the output of PrepareDisk action to CreateVM, automatically
|
13
|
+
detecting the dependency making sure to run the one before the other).
|
14
|
+
|
15
|
+
It also simulates a failure and demonstrates the Dynflow ability to rescue
|
16
|
+
from the error and consinue with the run.
|
17
|
+
|
18
|
+
Once the Sinatra web console starts, you can navigate to http://localhost:4567
|
19
|
+
to see what's happening in the Dynflow world.
|
20
|
+
|
21
|
+
DESC
|
22
|
+
|
23
|
+
require_relative 'example_helper'
|
2
24
|
|
3
25
|
module Orchestrate
|
4
26
|
|
27
|
+
|
5
28
|
class CreateInfrastructure < Dynflow::Action
|
6
29
|
|
7
30
|
def plan
|
@@ -16,7 +39,6 @@ module Orchestrate
|
|
16
39
|
:db_machine => 'host1',
|
17
40
|
:storage_machine => 'host2')
|
18
41
|
end
|
19
|
-
sleep 2
|
20
42
|
end
|
21
43
|
end
|
22
44
|
|
@@ -36,12 +58,19 @@ module Orchestrate
|
|
36
58
|
end
|
37
59
|
|
38
60
|
def finalize
|
39
|
-
|
61
|
+
# this is called after run methods of the actions in the
|
62
|
+
# execution plan were finished
|
40
63
|
end
|
41
64
|
|
42
65
|
end
|
43
66
|
|
44
|
-
class
|
67
|
+
class Base < Dynflow::Action
|
68
|
+
def sleep!
|
69
|
+
sleep(rand(2))
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class PrepareDisk < Base
|
45
74
|
|
46
75
|
input_format do
|
47
76
|
param :name
|
@@ -52,13 +81,13 @@ module Orchestrate
|
|
52
81
|
end
|
53
82
|
|
54
83
|
def run
|
55
|
-
sleep
|
84
|
+
sleep!
|
56
85
|
output[:path] = "/var/images/#{input[:name]}.img"
|
57
86
|
end
|
58
87
|
|
59
88
|
end
|
60
89
|
|
61
|
-
class CreateVM <
|
90
|
+
class CreateVM < Base
|
62
91
|
|
63
92
|
input_format do
|
64
93
|
param :name
|
@@ -70,25 +99,25 @@ module Orchestrate
|
|
70
99
|
end
|
71
100
|
|
72
101
|
def run
|
73
|
-
sleep
|
102
|
+
sleep!
|
74
103
|
output[:ip] = "192.168.100.#{rand(256)}"
|
75
104
|
end
|
76
105
|
|
77
106
|
end
|
78
107
|
|
79
|
-
class AddIPtoHosts <
|
108
|
+
class AddIPtoHosts < Base
|
80
109
|
|
81
110
|
input_format do
|
82
111
|
param :ip
|
83
112
|
end
|
84
113
|
|
85
114
|
def run
|
86
|
-
sleep
|
115
|
+
sleep!
|
87
116
|
end
|
88
117
|
|
89
118
|
end
|
90
119
|
|
91
|
-
class ConfigureMachine <
|
120
|
+
class ConfigureMachine < Base
|
92
121
|
|
93
122
|
input_format do
|
94
123
|
param :ip
|
@@ -98,24 +127,31 @@ module Orchestrate
|
|
98
127
|
|
99
128
|
def run
|
100
129
|
# for demonstration of resuming after error
|
101
|
-
if
|
102
|
-
|
130
|
+
if ExampleHelper.something_should_fail?
|
131
|
+
ExampleHelper.nothing_should_fail!
|
132
|
+
puts <<-MSG.gsub(/^.*\|/, '')
|
133
|
+
|
134
|
+
| Execution plan #{execution_plan_id} is failing
|
135
|
+
| You can resume it at http://localhost:4567/#{execution_plan_id}
|
136
|
+
|
137
|
+
MSG
|
103
138
|
raise "temporary unavailabe"
|
104
139
|
end
|
105
140
|
|
106
|
-
sleep
|
141
|
+
sleep!
|
107
142
|
end
|
108
143
|
|
109
144
|
end
|
145
|
+
end
|
110
146
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
@should_pass = true
|
147
|
+
if $0 == __FILE__
|
148
|
+
ExampleHelper.something_should_fail!
|
149
|
+
ExampleHelper.world.trigger(Orchestrate::CreateInfrastructure)
|
150
|
+
Thread.new do
|
151
|
+
9.times do
|
152
|
+
ExampleHelper.world.trigger(Orchestrate::CreateInfrastructure)
|
153
|
+
end
|
119
154
|
end
|
120
|
-
|
155
|
+
puts example_description
|
156
|
+
ExampleHelper.run_web_console
|
121
157
|
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative 'example_helper'
|
4
|
+
|
5
|
+
example_description = <<DESC
|
6
|
+
Orchestrate Evented Example
|
7
|
+
===========================
|
8
|
+
|
9
|
+
This example, how the `examples/orchestrate.rb` can be updated to not block
|
10
|
+
the threads while waiting for external tasks. In this cases, we usually wait
|
11
|
+
most of the time: and we can suspend the run of the action while waiting,
|
12
|
+
for the event. Therefore we suspend the action in the run, ask the world.clock
|
13
|
+
to wake us up few seconds later, so that the thread pool can do something useful
|
14
|
+
in the meantime.
|
15
|
+
|
16
|
+
Additional benefit besides being able to do more while waiting is allowing to
|
17
|
+
send external events to the action while it's suspended. One use case is being
|
18
|
+
able to cancel the action while it's running.
|
19
|
+
|
20
|
+
Once the Sinatra web console starts, you can navigate to http://localhost:4567
|
21
|
+
to see what's happening in the Dynflow world.
|
22
|
+
|
23
|
+
DESC
|
24
|
+
|
25
|
+
module OrchestrateEvented
|
26
|
+
|
27
|
+
class CreateInfrastructure < Dynflow::Action
|
28
|
+
|
29
|
+
def plan(get_stuck = false)
|
30
|
+
sequence do
|
31
|
+
concurrence do
|
32
|
+
plan_action(CreateMachine, 'host1', 'db', get_stuck: get_stuck)
|
33
|
+
plan_action(CreateMachine, 'host2', 'storage')
|
34
|
+
end
|
35
|
+
plan_action(CreateMachine,
|
36
|
+
'host3',
|
37
|
+
'web_server',
|
38
|
+
:db_machine => 'host1',
|
39
|
+
:storage_machine => 'host2')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class CreateMachine < Dynflow::Action
|
45
|
+
|
46
|
+
def plan(name, profile, config_options = {})
|
47
|
+
prepare_disk = plan_action(PrepareDisk, 'name' => name)
|
48
|
+
create_vm = plan_action(CreateVM,
|
49
|
+
:name => name,
|
50
|
+
:disk => prepare_disk.output['path'])
|
51
|
+
plan_action(AddIPtoHosts, :name => name, :ip => create_vm.output[:ip])
|
52
|
+
plan_action(ConfigureMachine,
|
53
|
+
:ip => create_vm.output[:ip],
|
54
|
+
:profile => profile,
|
55
|
+
:config_options => config_options)
|
56
|
+
plan_self(:name => name)
|
57
|
+
end
|
58
|
+
|
59
|
+
def finalize
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
class Base < Dynflow::Action
|
65
|
+
|
66
|
+
Finished = Algebrick.atom
|
67
|
+
|
68
|
+
def run(event = nil)
|
69
|
+
match(event,
|
70
|
+
(on Finished do
|
71
|
+
on_finish
|
72
|
+
end),
|
73
|
+
(on Dynflow::Action::Skip do
|
74
|
+
# do nothing
|
75
|
+
end),
|
76
|
+
(on nil do
|
77
|
+
suspend { |suspended_action| world.clock.ping suspended_action, rand(1), Finished }
|
78
|
+
end))
|
79
|
+
end
|
80
|
+
|
81
|
+
def on_finish
|
82
|
+
raise NotImplementedError
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
class PrepareDisk < Base
|
88
|
+
|
89
|
+
input_format do
|
90
|
+
param :name
|
91
|
+
end
|
92
|
+
|
93
|
+
output_format do
|
94
|
+
param :path
|
95
|
+
end
|
96
|
+
|
97
|
+
def on_finish
|
98
|
+
output[:path] = "/var/images/#{input[:name]}.img"
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
class CreateVM < Base
|
104
|
+
|
105
|
+
input_format do
|
106
|
+
param :name
|
107
|
+
param :disk
|
108
|
+
end
|
109
|
+
|
110
|
+
output_format do
|
111
|
+
param :ip
|
112
|
+
end
|
113
|
+
|
114
|
+
def on_finish
|
115
|
+
output[:ip] = "192.168.100.#{rand(256)}"
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
class AddIPtoHosts < Base
|
121
|
+
|
122
|
+
input_format do
|
123
|
+
param :ip
|
124
|
+
end
|
125
|
+
|
126
|
+
def on_finish
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
class ConfigureMachine < Base
|
132
|
+
|
133
|
+
# thanks to this Dynflow knows this action can be politely
|
134
|
+
# asked to get canceled
|
135
|
+
include ::Dynflow::Action::Cancellable
|
136
|
+
|
137
|
+
input_format do
|
138
|
+
param :ip
|
139
|
+
param :profile
|
140
|
+
param :config_options
|
141
|
+
end
|
142
|
+
|
143
|
+
def run(event = nil)
|
144
|
+
if event == Dynflow::Action::Cancellable::Cancel
|
145
|
+
output[:message] = "I was cancelled but we don't care"
|
146
|
+
else
|
147
|
+
super
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def on_finish
|
152
|
+
if input[:config_options][:get_stuck]
|
153
|
+
puts <<-MSG.gsub(/^.*\|/, '')
|
154
|
+
|
155
|
+
| Execution plan #{execution_plan_id} got stuck
|
156
|
+
| You can cancel the stucked step at http://localhost:4567/#{execution_plan_id}
|
157
|
+
|
158
|
+
MSG
|
159
|
+
# we suspend the action but don't plan the wakeup event,
|
160
|
+
# causing it to wait forever (till we cancel it)
|
161
|
+
suspend
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
|
169
|
+
if $0 == __FILE__
|
170
|
+
ExampleHelper.world.trigger(OrchestrateEvented::CreateInfrastructure)
|
171
|
+
ExampleHelper.world.trigger(OrchestrateEvented::CreateInfrastructure, true)
|
172
|
+
puts example_description
|
173
|
+
ExampleHelper.run_web_console
|
174
|
+
end
|