dynflow 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/Gemfile +9 -0
- data/MIT-LICENSE +20 -0
- data/README.md +151 -0
- data/Rakefile +7 -0
- data/dynflow.gemspec +23 -0
- data/examples/events.rb +71 -0
- data/examples/workflow.rb +140 -0
- data/lib/dynflow.rb +11 -0
- data/lib/dynflow/action.rb +121 -0
- data/lib/dynflow/bus.rb +56 -0
- data/lib/dynflow/dispatcher.rb +36 -0
- data/lib/dynflow/logger.rb +34 -0
- data/lib/dynflow/message.rb +38 -0
- data/lib/dynflow/orch_request.rb +14 -0
- data/lib/dynflow/orch_response.rb +5 -0
- data/lib/dynflow/version.rb +3 -0
- data/test/action_test.rb +24 -0
- data/test/bus_test.rb +55 -0
- data/test/dispatcher_test.rb +108 -0
- data/test/test_helper.rb +83 -0
- metadata +135 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Gemfile.lock
|
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2012 Pavel Pokorný
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
DYNamic workFLOW
|
2
|
+
================
|
3
|
+
|
4
|
+
In traditional workflow engines, you specify a static workflow and
|
5
|
+
then run it with various inputs. Dynflow takes different approach.
|
6
|
+
|
7
|
+
You specify the inputs and the workflow is generated on the fly. You
|
8
|
+
can either specify the steps explicitly or subscribe one action to
|
9
|
+
another. This is suitable for plugin architecture, where you can't
|
10
|
+
write the whole process on one place.
|
11
|
+
|
12
|
+
Dynflow doesn't differentiate between workflow and action. Instead,
|
13
|
+
every action can populate another actions, effectively producing the
|
14
|
+
resulting set of steps.
|
15
|
+
|
16
|
+
The whole execution is done in three phases:
|
17
|
+
|
18
|
+
1. *Planning phase*
|
19
|
+
|
20
|
+
Construct the execution plan for the workflow. It's invoked by
|
21
|
+
calling `trigger` on an action. Two mechanisms are used to get the set
|
22
|
+
of actions to be executed:
|
23
|
+
|
24
|
+
a. explicit calls of `plan_action` methods in the `plan` method
|
25
|
+
|
26
|
+
b. implicit associations: an action A subscribes to an action B,
|
27
|
+
which means that the action A is executed whenever the action B
|
28
|
+
occurs.
|
29
|
+
|
30
|
+
The output of this phase is a set of actions and their inputs.
|
31
|
+
|
32
|
+
2. *Execution phase*
|
33
|
+
|
34
|
+
The plan is being executed step by step, calling the run method of
|
35
|
+
an action with corresponding input. The results of every action are
|
36
|
+
written into output attribute.
|
37
|
+
|
38
|
+
The run method should be stateless, with all the needed information
|
39
|
+
included in the input from planning phase. This allows us to
|
40
|
+
control the workflow execution: the state of every action can be
|
41
|
+
serialized therefore the workflow itself can be persisted. This makes
|
42
|
+
it easy to recover from failed actions by rerunning it.
|
43
|
+
|
44
|
+
3. *Finalization phase*
|
45
|
+
|
46
|
+
Take the results from the execution phase and perform some additional
|
47
|
+
tasks. This is suitable for example for recording the results into
|
48
|
+
database.
|
49
|
+
|
50
|
+
Every action can participate in every phase.
|
51
|
+
|
52
|
+
Example
|
53
|
+
-------
|
54
|
+
|
55
|
+
One code snippet is worth 1000 words:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
# The anatomy of action class
|
59
|
+
|
60
|
+
# every action needs to inherit from Dynflow::Action
|
61
|
+
class Action < Dynflow::Action
|
62
|
+
|
63
|
+
# OPTIONAL: the input format for the execution phase of this action
|
64
|
+
# (https://github.com/iNecas/apipie-params for more details.
|
65
|
+
# Validations can be performed against this description (turned off
|
66
|
+
# for now)
|
67
|
+
input_format do
|
68
|
+
param :id, Integer
|
69
|
+
param :name, String
|
70
|
+
end
|
71
|
+
|
72
|
+
# OPTIONAL: every action can produce an output in the execution
|
73
|
+
# phase. This allows to describe the output.
|
74
|
+
output_format do
|
75
|
+
param :uuid, String
|
76
|
+
end
|
77
|
+
|
78
|
+
# OPTIONAL: this specifies that this action should be performed when
|
79
|
+
# AnotherAction is triggered.
|
80
|
+
def self.subscribe
|
81
|
+
AnotherAction
|
82
|
+
end
|
83
|
+
|
84
|
+
# OPTIONAL: executed during the planning phase. It's possible to
|
85
|
+
# specify explicitly the workflow here. By default it schedules just
|
86
|
+
# this action.
|
87
|
+
def plan(object_1, object_2)
|
88
|
+
# +plan_action+ schedules the SubAction to be part of this
|
89
|
+
# workflow
|
90
|
+
# the +object_1+ is passed to the +SubAction#plan+ method.
|
91
|
+
plan_action SubAction, object_1
|
92
|
+
# we can specify, where in the workflow this action should be
|
93
|
+
# placed, as well as prepare the input.
|
94
|
+
plan_self {'id' => object_2.id, 'name' => object_2.name}
|
95
|
+
end
|
96
|
+
|
97
|
+
# OPTIONAL: run the execution part of this action. Transform the
|
98
|
+
# data from +input+ to +output+. When not specified, the action is
|
99
|
+
# not used in the execution phase.
|
100
|
+
def run
|
101
|
+
output['uuid'] = "#{input['name']}-#{input['id']}"
|
102
|
+
end
|
103
|
+
|
104
|
+
# OPTIONAL: finalize the action after the execution phase finishes.
|
105
|
+
# in the +input+ and +output+ attributes are available the data from
|
106
|
+
# execution phase. in the +outputs+ argument, all the execution
|
107
|
+
# phase actions are available, each providing its input and output.
|
108
|
+
def finalize(outputs)
|
109
|
+
puts output['uuid']
|
110
|
+
end
|
111
|
+
end
|
112
|
+
```
|
113
|
+
|
114
|
+
One can generate the execution plan for an action without actually
|
115
|
+
running it:
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
pp Publish.plan(short_article)
|
119
|
+
# the expanded workflow is:
|
120
|
+
# [
|
121
|
+
# [Publish, {"title"=>"Short", "body"=>"Short"}],
|
122
|
+
# [Review, {"title"=>"Short", "body"=>"Short"}],
|
123
|
+
# [Print, {"title"=>"Short", "body"=>"Short", "color"=>false}]
|
124
|
+
# ]
|
125
|
+
```
|
126
|
+
|
127
|
+
Therefore it's suitable for the plan methods to not have any side
|
128
|
+
effects (except of database writes that can be roll-backed)
|
129
|
+
|
130
|
+
In the finalization phase, `finalize` method is called on every action
|
131
|
+
if defined. The order is the same as in the execution plan.
|
132
|
+
|
133
|
+
Every action should be as atomic as possible, providing better
|
134
|
+
granularity when manipulating the process. Since every action can be
|
135
|
+
subscribed by another one, adding new behaviour to an existing
|
136
|
+
workflow is really simple.
|
137
|
+
|
138
|
+
The input and output format can be used for defining the interface
|
139
|
+
that other developers can use when extending the workflows.
|
140
|
+
|
141
|
+
See the examples directory for more complete examples.
|
142
|
+
|
143
|
+
License
|
144
|
+
-------
|
145
|
+
|
146
|
+
MIT
|
147
|
+
|
148
|
+
Author
|
149
|
+
------
|
150
|
+
|
151
|
+
Ivan Nečas
|
data/Rakefile
ADDED
data/dynflow.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "dynflow/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "dynflow"
|
7
|
+
s.version = Dynflow::VERSION
|
8
|
+
s.authors = ["Ivan Necas"]
|
9
|
+
s.email = ["inecas@redhat.com"]
|
10
|
+
s.homepage = "http://github.com/iNecas/eventum"
|
11
|
+
s.summary = "DYNamic workFLOW engine"
|
12
|
+
s.description = "Generate and executed workflows dynamically based "+
|
13
|
+
"on input data and leave it open for others to jump into it as well"
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
|
19
|
+
s.add_dependency "activesupport"
|
20
|
+
s.add_dependency "multi_json"
|
21
|
+
s.add_dependency "apipie-params"
|
22
|
+
s.add_development_dependency "minitest"
|
23
|
+
end
|
data/examples/events.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# Shows how Dynflow can be used for events architecture: actions are
|
2
|
+
# subscribed to an event. When the event is triggered all the
|
3
|
+
# subscribed actions are preformed.
|
4
|
+
|
5
|
+
$:.unshift(File.expand_path('../../lib', __FILE__))
|
6
|
+
|
7
|
+
require 'dynflow'
|
8
|
+
require 'pp'
|
9
|
+
|
10
|
+
# this is an event that can be triggered.
|
11
|
+
# it has an input format so that the interface is given
|
12
|
+
# TODO: the validations are turned off right now
|
13
|
+
class Click < Dynflow::Action
|
14
|
+
input_format do
|
15
|
+
param :x, Integer
|
16
|
+
param :y, Integer
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# SayHello subscibes to the event: it's run when the event is triggered
|
21
|
+
class SayHello < Dynflow::Action
|
22
|
+
|
23
|
+
def self.subscribe
|
24
|
+
Click
|
25
|
+
end
|
26
|
+
|
27
|
+
def run
|
28
|
+
puts "Hello World"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# we can subscribe more actions to an event
|
33
|
+
class SayPosition < Dynflow::Action
|
34
|
+
|
35
|
+
def self.subscribe
|
36
|
+
Click
|
37
|
+
end
|
38
|
+
|
39
|
+
def run
|
40
|
+
puts "your position is [#{input['x']} - #{input['y']}]"
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
# we can even subscribe to an action that is subscribed to an event
|
46
|
+
class SayGoodbye < Dynflow::Action
|
47
|
+
|
48
|
+
def self.subscribe
|
49
|
+
SayPosition
|
50
|
+
end
|
51
|
+
|
52
|
+
def run
|
53
|
+
puts "Good Bye"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
Click.trigger('x' => 5, 'y' => 4)
|
58
|
+
# gives us:
|
59
|
+
# Hello World
|
60
|
+
# your position is [5 - 4]
|
61
|
+
# Good Bye
|
62
|
+
|
63
|
+
pp Click.plan('x' => 5, 'y' => 4)
|
64
|
+
# returns the execution plan for the event (nothing is triggered):
|
65
|
+
# [
|
66
|
+
# since the event is action as well, it could have a run method
|
67
|
+
# [Click, {"x"=>5, "y"=>4}],
|
68
|
+
# [SayHello, {"x"=>5, "y"=>4}],
|
69
|
+
# [SayPosition, {"x"=>5, "y"=>4}],
|
70
|
+
# [SayGoodbye, {"x"=>5, "y"=>4}]
|
71
|
+
# ]
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# Shows how Dynflow can be used for dynamic workflow definition
|
2
|
+
# and execution.
|
3
|
+
# In a planning phase of an action, a sub-action can be planned as
|
4
|
+
# well.
|
5
|
+
|
6
|
+
|
7
|
+
$:.unshift(File.expand_path('../../lib', __FILE__))
|
8
|
+
|
9
|
+
require 'dynflow'
|
10
|
+
require 'pp'
|
11
|
+
|
12
|
+
class Article < Struct.new(:title, :body, :color); end
|
13
|
+
|
14
|
+
class Publish < Dynflow::Action
|
15
|
+
input_format do
|
16
|
+
param :title, Integer
|
17
|
+
param :body, Integer
|
18
|
+
end
|
19
|
+
|
20
|
+
# plan can take arbitrary arguments. The args are passed from the
|
21
|
+
# trigger method.
|
22
|
+
def plan(article)
|
23
|
+
# we can explicitly plan a subaction
|
24
|
+
plan_self 'title' => article.title, 'body' => article.body
|
25
|
+
plan_action Review, article
|
26
|
+
end
|
27
|
+
|
28
|
+
def run
|
29
|
+
puts 'Starting'
|
30
|
+
end
|
31
|
+
|
32
|
+
# after all actions are run, there is a finishing phase. All the
|
33
|
+
# actions with +finished+ action defined are called, passing all the
|
34
|
+
# performed actions (with inputs and outputs)
|
35
|
+
def finalize(outputs)
|
36
|
+
printer_action = outputs.find { |o| o.is_a? Print }
|
37
|
+
puts "Printer says '#{printer_action.output['message']}'"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Review < Dynflow::Action
|
42
|
+
|
43
|
+
# the actions can provide an output for the finalizing phase
|
44
|
+
output_format do
|
45
|
+
param :rating, Integer
|
46
|
+
end
|
47
|
+
|
48
|
+
# the plan method takes the same arguments as the parent action
|
49
|
+
def plan(article)
|
50
|
+
# in the input attribute the input for the parent action is
|
51
|
+
# available
|
52
|
+
plan_self input
|
53
|
+
end
|
54
|
+
|
55
|
+
# if no plan method given, the input is the same as the action that
|
56
|
+
# triggered it
|
57
|
+
def run
|
58
|
+
puts "Reviewing #{input['title']}"
|
59
|
+
raise "Too Short" if input['body'].size < 6
|
60
|
+
output['rating'] = input['body'].size
|
61
|
+
end
|
62
|
+
|
63
|
+
def finalize(outputs)
|
64
|
+
# +input+ and +output+ attributes are available in the finalizing
|
65
|
+
# phase as well.
|
66
|
+
puts "The rating was #{output['rating']}"
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
class Print < Dynflow::Action
|
72
|
+
|
73
|
+
input_format do
|
74
|
+
param :title, Integer
|
75
|
+
param :body, Integer
|
76
|
+
param :color, :boolean
|
77
|
+
end
|
78
|
+
|
79
|
+
output_format do
|
80
|
+
param :message, String
|
81
|
+
end
|
82
|
+
|
83
|
+
# if needed, we can subscribe to an action instead of explicitly
|
84
|
+
# specifying it in the plan method. Suitable for plugin architecture.
|
85
|
+
def self.subscribe
|
86
|
+
Review # sucessful review means we can print
|
87
|
+
end
|
88
|
+
|
89
|
+
def plan(article)
|
90
|
+
plan_self input.merge('color' => article.color)
|
91
|
+
end
|
92
|
+
|
93
|
+
def run
|
94
|
+
if input['color']
|
95
|
+
puts "Printing in color"
|
96
|
+
else
|
97
|
+
puts "Printing blank&white"
|
98
|
+
end
|
99
|
+
output['message'] = "Here you are"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
short_article = Article.new('Short', 'Short', false)
|
104
|
+
long_article = Article.new('Long', 'This is long', false)
|
105
|
+
colorful_article = Article.new('Long Color', 'This is long in color', true)
|
106
|
+
|
107
|
+
pp Publish.plan(short_article)
|
108
|
+
# the expanded workflow is:
|
109
|
+
# [
|
110
|
+
# [Publish, {"title"=>"Short", "body"=>"Short"}],
|
111
|
+
# [Review, {"title"=>"Short", "body"=>"Short"}],
|
112
|
+
# [Print, {"title"=>"Short", "body"=>"Short", "color"=>false}]
|
113
|
+
# ]
|
114
|
+
|
115
|
+
begin
|
116
|
+
Publish.trigger(short_article)
|
117
|
+
rescue => e
|
118
|
+
puts e.message
|
119
|
+
end
|
120
|
+
# Produces:
|
121
|
+
# Starting
|
122
|
+
# Reviewing Short
|
123
|
+
# Too Short
|
124
|
+
|
125
|
+
Publish.trigger(long_article)
|
126
|
+
# Produces:
|
127
|
+
# Starting
|
128
|
+
# Reviewing Long
|
129
|
+
# Printing blank&white
|
130
|
+
# Printer says 'Here you are'
|
131
|
+
# The rating was 12
|
132
|
+
|
133
|
+
|
134
|
+
Publish.trigger(colorful_article)
|
135
|
+
# Produces:
|
136
|
+
# Starting
|
137
|
+
# Reviewing Long Color
|
138
|
+
# Printing in color
|
139
|
+
# Printer says 'Here you are'
|
140
|
+
# The rating was 21
|
data/lib/dynflow.rb
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
module Dynflow
|
2
|
+
class Action < Message
|
3
|
+
|
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 :from_subscription
|
9
|
+
|
10
|
+
def self.inherited(child)
|
11
|
+
self.actions << child
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.actions
|
15
|
+
@actions ||= []
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.subscribe
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.require
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(input, output = {})
|
27
|
+
# for preparation phase
|
28
|
+
@execution_plan = []
|
29
|
+
|
30
|
+
output ||= {}
|
31
|
+
super('input' => input, 'output' => output)
|
32
|
+
end
|
33
|
+
|
34
|
+
def input
|
35
|
+
@data['input']
|
36
|
+
end
|
37
|
+
|
38
|
+
def input=(input)
|
39
|
+
@data['input'] = input
|
40
|
+
end
|
41
|
+
|
42
|
+
def output
|
43
|
+
@data['output']
|
44
|
+
end
|
45
|
+
|
46
|
+
# the block contains the expression in Apipie::Params::DSL
|
47
|
+
# describing the format of message
|
48
|
+
def self.input_format(&block)
|
49
|
+
if block
|
50
|
+
@input_format_block = block
|
51
|
+
elsif @input_format_block
|
52
|
+
@input_format ||= Apipie::Params::Description.define(&@input_format_block)
|
53
|
+
else
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# the block contains the expression in Apipie::Params::DSL
|
59
|
+
# describing the format of message
|
60
|
+
def self.output_format(&block)
|
61
|
+
if block
|
62
|
+
@output_format_block = block
|
63
|
+
elsif @output_format_block
|
64
|
+
@output_format ||= Apipie::Params::Description.define(&@output_format_block)
|
65
|
+
else
|
66
|
+
nil
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.trigger(*args)
|
71
|
+
Dynflow::Bus.trigger(self.plan(*args))
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.plan(*args)
|
75
|
+
action = self.new({})
|
76
|
+
yield action if block_given?
|
77
|
+
action.plan(*args)
|
78
|
+
action.add_subscriptions(*args)
|
79
|
+
action.execution_plan
|
80
|
+
end
|
81
|
+
|
82
|
+
# for subscribed actions: by default take the input of the
|
83
|
+
# subscribed action
|
84
|
+
def plan(*args)
|
85
|
+
if from_subscription
|
86
|
+
# if the action is triggered by subscription, by default use the
|
87
|
+
# input of parent action
|
88
|
+
plan_self(self.input)
|
89
|
+
else
|
90
|
+
# in this case, the action was triggered by plan_action. Use
|
91
|
+
# the argument specified there.
|
92
|
+
plan_self(args.first)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def plan_self(input)
|
97
|
+
self.input = input
|
98
|
+
@execution_plan << [self.class, input]
|
99
|
+
end
|
100
|
+
|
101
|
+
def plan_action(action_class, *args)
|
102
|
+
sub_action_plan = action_class.plan(*args) do |action|
|
103
|
+
action.input = self.input
|
104
|
+
end
|
105
|
+
@execution_plan.concat(sub_action_plan)
|
106
|
+
end
|
107
|
+
|
108
|
+
def add_subscriptions(*plan_args)
|
109
|
+
@execution_plan.concat(Dispatcher.execution_plan_for(self, *plan_args))
|
110
|
+
end
|
111
|
+
|
112
|
+
def execution_plan
|
113
|
+
@execution_plan
|
114
|
+
end
|
115
|
+
|
116
|
+
def validate!
|
117
|
+
self.clss.output_format.validate!(@data['output'])
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
end
|
data/lib/dynflow/bus.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
require 'forwardable'
|
3
|
+
module Dynflow
|
4
|
+
class Bus
|
5
|
+
|
6
|
+
class << self
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
def_delegators :impl, :wait_for, :process, :trigger, :finalize
|
10
|
+
|
11
|
+
def impl
|
12
|
+
@impl ||= Bus::MemoryBus.new
|
13
|
+
end
|
14
|
+
attr_writer :impl
|
15
|
+
end
|
16
|
+
|
17
|
+
def finalize(outputs)
|
18
|
+
outputs.each do |action|
|
19
|
+
if action.respond_to?(:finalize)
|
20
|
+
action.finalize(outputs)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def process(action_class, input, output = nil)
|
26
|
+
# TODO: here goes the message validation
|
27
|
+
action = action_class.new(input, output)
|
28
|
+
action.run if action.respond_to?(:run)
|
29
|
+
return action
|
30
|
+
end
|
31
|
+
|
32
|
+
def wait_for(*args)
|
33
|
+
raise NotImplementedError, 'Abstract method'
|
34
|
+
end
|
35
|
+
|
36
|
+
def logger
|
37
|
+
@logger ||= Dynflow::Logger.new(self.class)
|
38
|
+
end
|
39
|
+
|
40
|
+
class MemoryBus < Bus
|
41
|
+
|
42
|
+
def initialize
|
43
|
+
super
|
44
|
+
end
|
45
|
+
|
46
|
+
def trigger(execution_plan)
|
47
|
+
outputs = []
|
48
|
+
execution_plan.each do |(action_class, input)|
|
49
|
+
outputs << self.process(action_class, input)
|
50
|
+
end
|
51
|
+
self.finalize(outputs)
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Dynflow
|
2
|
+
class Dispatcher
|
3
|
+
class << self
|
4
|
+
def finalizers
|
5
|
+
@finalizers ||= Hash.new { |h, k| h[k] = [] }
|
6
|
+
end
|
7
|
+
|
8
|
+
def subscribed_actions(action)
|
9
|
+
Action.actions.find_all do |sub_action|
|
10
|
+
case sub_action.subscribe
|
11
|
+
when Hash
|
12
|
+
sub_action.subscribe.keys.include?(action.class)
|
13
|
+
when Array
|
14
|
+
sub_action.subscribe.include?(action.class)
|
15
|
+
else
|
16
|
+
sub_action.subscribe == action.class
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def execution_plan_for(action, *plan_args)
|
22
|
+
ordered_actions = subscribed_actions(action).sort_by(&:name)
|
23
|
+
|
24
|
+
execution_plan = []
|
25
|
+
ordered_actions.each do |action_class|
|
26
|
+
sub_action_plan = action_class.plan(*plan_args) do |sub_action|
|
27
|
+
sub_action.input = action.input
|
28
|
+
sub_action.from_subscription = true
|
29
|
+
end
|
30
|
+
execution_plan.concat(sub_action_plan)
|
31
|
+
end
|
32
|
+
return execution_plan
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
module Dynflow
|
5
|
+
class Logger
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
def_delegators :@impl, :debug, :info, :warn; :error
|
9
|
+
|
10
|
+
class DummyLogger < ::Logger
|
11
|
+
def initialize(identifier)
|
12
|
+
super(nil)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
def initialize(identifier, impl = nil)
|
18
|
+
@impl = self.class.logger_class.new(identifier)
|
19
|
+
end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
|
23
|
+
def logger_class
|
24
|
+
unless @logger_class
|
25
|
+
@logger_class ||= DummyLogger
|
26
|
+
end
|
27
|
+
return @logger_class
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_writer :logger_class
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
3
|
+
require 'active_support/core_ext/string/inflections'
|
4
|
+
require 'apipie-params'
|
5
|
+
|
6
|
+
module Dynflow
|
7
|
+
class Message
|
8
|
+
|
9
|
+
def ==(other)
|
10
|
+
self.encode == other.encode
|
11
|
+
end
|
12
|
+
|
13
|
+
extend Forwardable
|
14
|
+
|
15
|
+
def_delegators :@data, '[]', '[]='
|
16
|
+
|
17
|
+
attr_reader :data
|
18
|
+
|
19
|
+
def initialize(data = {})
|
20
|
+
@data = data.with_indifferent_access
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
def self.decode(data)
|
25
|
+
ret = data['message_type'].constantize.allocate
|
26
|
+
ret.instance_variable_set("@data", data['data'])
|
27
|
+
return ret
|
28
|
+
end
|
29
|
+
|
30
|
+
def encode
|
31
|
+
{
|
32
|
+
'message_type' => self.class.name,
|
33
|
+
'data' => @data
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Dynflow
|
2
|
+
class OrchRequest < Message
|
3
|
+
def self.response_class
|
4
|
+
unless self.name =~ /::Request\Z/
|
5
|
+
raise "Unexpected class name, #{self.name} expected to end with ::Request"
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
self.name.sub(/::Request\Z/, '::Response').constantize
|
9
|
+
rescue NameError => e
|
10
|
+
OrchResponse
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/test/action_test.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Dynflow
|
4
|
+
class CloneRepo < Action
|
5
|
+
|
6
|
+
output_format do
|
7
|
+
param :id, String
|
8
|
+
end
|
9
|
+
|
10
|
+
def run
|
11
|
+
output['id'] = input['name']
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
class CloneRepoTest < ParticipantTestCase
|
17
|
+
|
18
|
+
def test_action
|
19
|
+
action = run_action(CloneRepo, {:name => "zoo"})
|
20
|
+
assert_equal(action.output['id'], "zoo")
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
data/test/bus_test.rb
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
module Dynflow
|
5
|
+
class BusTest < BusTestCase
|
6
|
+
class Promotion < Action
|
7
|
+
|
8
|
+
def plan(repo_names, package_names)
|
9
|
+
repo_names.each do |repo_name|
|
10
|
+
plan_action(CloneRepo, {'name' => repo_name})
|
11
|
+
end
|
12
|
+
|
13
|
+
package_names.each do |package_name|
|
14
|
+
plan_action(ClonePackage, {'name' => package_name})
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
class CloneRepo < Action
|
21
|
+
|
22
|
+
input_format do
|
23
|
+
param :name, String
|
24
|
+
end
|
25
|
+
|
26
|
+
output_format do
|
27
|
+
param :id, String
|
28
|
+
end
|
29
|
+
|
30
|
+
def run
|
31
|
+
output['id'] = input['name']
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
def execution_plan
|
37
|
+
[
|
38
|
+
[CloneRepo, {'name' => 'zoo'}],
|
39
|
+
[CloneRepo, {'name' => 'foo'}],
|
40
|
+
]
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_optimistic_case
|
44
|
+
expect_input(CloneRepo, {'name' => 'zoo'}, {'id' => '123'})
|
45
|
+
expect_input(CloneRepo, {'name' => 'foo'}, {'id' => '456'})
|
46
|
+
first_action, second_action = assert_scenario
|
47
|
+
|
48
|
+
assert_equal({'name' => 'zoo'}, first_action.input)
|
49
|
+
assert_equal({'id' => '123'}, first_action.output)
|
50
|
+
assert_equal({'name' => 'foo'}, second_action.input)
|
51
|
+
assert_equal({'id' => '456'}, second_action.output)
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Dynflow
|
4
|
+
|
5
|
+
describe Dispatcher do
|
6
|
+
class Promotion < Action
|
7
|
+
|
8
|
+
def plan(repo_names, package_names)
|
9
|
+
repo_names.each do |repo_name|
|
10
|
+
plan_action(CloneRepo, {'name' => repo_name})
|
11
|
+
end
|
12
|
+
|
13
|
+
package_names.each do |package_name|
|
14
|
+
plan_action(ClonePackage, {'name' => package_name})
|
15
|
+
end
|
16
|
+
|
17
|
+
plan_self('actions' => repo_names.size + package_names.size)
|
18
|
+
end
|
19
|
+
|
20
|
+
input_format do
|
21
|
+
param :actions, Integer
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
class PromotionObserver < Action
|
27
|
+
|
28
|
+
def self.subscribe
|
29
|
+
Promotion
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
class CloneRepo < Action
|
35
|
+
|
36
|
+
input_format do
|
37
|
+
param :name, String
|
38
|
+
end
|
39
|
+
|
40
|
+
output_format do
|
41
|
+
param :id, String
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
class ClonePackage < Action
|
47
|
+
|
48
|
+
input_format do
|
49
|
+
param :name, String
|
50
|
+
end
|
51
|
+
|
52
|
+
output_format do
|
53
|
+
param :id, String
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
class UpdateIndex < Action
|
59
|
+
|
60
|
+
def self.subscribe
|
61
|
+
ClonePackage
|
62
|
+
end
|
63
|
+
|
64
|
+
def plan(input)
|
65
|
+
plan_action(YetAnotherAction, {'hello' => 'world'})
|
66
|
+
super
|
67
|
+
end
|
68
|
+
|
69
|
+
output_format do
|
70
|
+
param :indexed_name, String
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
class YetAnotherAction < Action
|
76
|
+
|
77
|
+
input_format do
|
78
|
+
param :name, String
|
79
|
+
param :hello, String
|
80
|
+
end
|
81
|
+
|
82
|
+
output_format do
|
83
|
+
param :hello, String
|
84
|
+
end
|
85
|
+
|
86
|
+
def plan(arg)
|
87
|
+
plan_self(input.merge(arg))
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
it "builds the execution plan" do
|
93
|
+
execution_plan = Promotion.plan(['zoo', 'foo'], ['elephant'])
|
94
|
+
expected_plan =
|
95
|
+
[
|
96
|
+
[CloneRepo, {'name' => 'zoo'}],
|
97
|
+
[CloneRepo, {'name' => 'foo'}],
|
98
|
+
[ClonePackage, {'name' => 'elephant'}],
|
99
|
+
[YetAnotherAction, {'name' => 'elephant', 'hello' => 'world'}],
|
100
|
+
[UpdateIndex, {'name' => 'elephant'}],
|
101
|
+
[Promotion, {'actions' => 3 }],
|
102
|
+
[PromotionObserver, {'actions' => 3 }]
|
103
|
+
]
|
104
|
+
execution_plan.must_equal expected_plan
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'minitest/spec'
|
3
|
+
require 'dynflow'
|
4
|
+
|
5
|
+
BUS_IMPL = Dynflow::Bus::MemoryBus
|
6
|
+
|
7
|
+
class TestBus < BUS_IMPL
|
8
|
+
|
9
|
+
def initialize(expected_scenario)
|
10
|
+
super()
|
11
|
+
@expected_scenario = expected_scenario
|
12
|
+
end
|
13
|
+
|
14
|
+
def process(action_class, input, output = nil, stub = true)
|
15
|
+
expected = @expected_scenario.shift
|
16
|
+
if action_class == TestScenarioFinalizer || !stub || output
|
17
|
+
return super(action_class, input, output)
|
18
|
+
elsif action_class.name == expected[:action_class].name && input == expected[:input]
|
19
|
+
return action_class.new(expected[:input], expected[:output])
|
20
|
+
else
|
21
|
+
raise "Unexpected input. Expected #{expected[:action_class]} #{expected[:input].inspect}, got #{action_class} #{input.inspect}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
class TestScenarioFinalizer < Dynflow::Action
|
28
|
+
|
29
|
+
class << self
|
30
|
+
|
31
|
+
def recorded_outputs
|
32
|
+
@recorded_outputs
|
33
|
+
end
|
34
|
+
|
35
|
+
def init_recorded_outputs
|
36
|
+
@recorded_outputs = []
|
37
|
+
end
|
38
|
+
|
39
|
+
def save_recorded_outputs(recorded_outputs)
|
40
|
+
@recorded_outputs = recorded_outputs
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
def finalize(outputs)
|
46
|
+
self.class.save_recorded_outputs(outputs)
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
class BusTestCase < Test::Unit::TestCase
|
52
|
+
|
53
|
+
def setup
|
54
|
+
@expected_scenario = []
|
55
|
+
end
|
56
|
+
|
57
|
+
def expect_input(action_class, input, output)
|
58
|
+
@expected_scenario << {
|
59
|
+
:action_class => action_class,
|
60
|
+
:input => input,
|
61
|
+
:output => output
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
def assert_scenario
|
66
|
+
Dynflow::Bus.impl = TestBus.new(@expected_scenario)
|
67
|
+
event_outputs = nil
|
68
|
+
TestScenarioFinalizer.init_recorded_outputs
|
69
|
+
execution_plan = self.execution_plan
|
70
|
+
execution_plan << [TestScenarioFinalizer, {}]
|
71
|
+
Dynflow::Bus.trigger(execution_plan)
|
72
|
+
return TestScenarioFinalizer.recorded_outputs
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class ParticipantTestCase < Test::Unit::TestCase
|
77
|
+
|
78
|
+
def run_action(action_class, input)
|
79
|
+
Dynflow::Bus.impl = Dynflow::Bus.new
|
80
|
+
output = Dynflow::Bus.process(action_class, input)
|
81
|
+
return output
|
82
|
+
end
|
83
|
+
end
|
metadata
ADDED
@@ -0,0 +1,135 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dynflow
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ivan Necas
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-04-24 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activesupport
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: multi_json
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: apipie-params
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: minitest
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
description: Generate and executed workflows dynamically based on input data and leave
|
79
|
+
it open for others to jump into it as well
|
80
|
+
email:
|
81
|
+
- inecas@redhat.com
|
82
|
+
executables: []
|
83
|
+
extensions: []
|
84
|
+
extra_rdoc_files: []
|
85
|
+
files:
|
86
|
+
- .gitignore
|
87
|
+
- Gemfile
|
88
|
+
- MIT-LICENSE
|
89
|
+
- README.md
|
90
|
+
- Rakefile
|
91
|
+
- dynflow.gemspec
|
92
|
+
- examples/events.rb
|
93
|
+
- examples/workflow.rb
|
94
|
+
- lib/dynflow.rb
|
95
|
+
- lib/dynflow/action.rb
|
96
|
+
- lib/dynflow/bus.rb
|
97
|
+
- lib/dynflow/dispatcher.rb
|
98
|
+
- lib/dynflow/logger.rb
|
99
|
+
- lib/dynflow/message.rb
|
100
|
+
- lib/dynflow/orch_request.rb
|
101
|
+
- lib/dynflow/orch_response.rb
|
102
|
+
- lib/dynflow/version.rb
|
103
|
+
- test/action_test.rb
|
104
|
+
- test/bus_test.rb
|
105
|
+
- test/dispatcher_test.rb
|
106
|
+
- test/test_helper.rb
|
107
|
+
homepage: http://github.com/iNecas/eventum
|
108
|
+
licenses: []
|
109
|
+
post_install_message:
|
110
|
+
rdoc_options: []
|
111
|
+
require_paths:
|
112
|
+
- lib
|
113
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
114
|
+
none: false
|
115
|
+
requirements:
|
116
|
+
- - ! '>='
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
120
|
+
none: false
|
121
|
+
requirements:
|
122
|
+
- - ! '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
requirements: []
|
126
|
+
rubyforge_project:
|
127
|
+
rubygems_version: 1.8.25
|
128
|
+
signing_key:
|
129
|
+
specification_version: 3
|
130
|
+
summary: DYNamic workFLOW engine
|
131
|
+
test_files:
|
132
|
+
- test/action_test.rb
|
133
|
+
- test/bus_test.rb
|
134
|
+
- test/dispatcher_test.rb
|
135
|
+
- test/test_helper.rb
|