dynflow 0.6.2 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
![Screenshot](doc/images/screenshot.png)
|
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
|