dynflow 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +1 -0
- data/lib/dynflow.rb +1 -0
- data/lib/dynflow/action.rb +5 -3
- data/lib/dynflow/action/finalize_phase.rb +3 -1
- data/lib/dynflow/action/plan_phase.rb +4 -2
- data/lib/dynflow/action/run_phase.rb +3 -1
- data/lib/dynflow/daemon.rb +1 -0
- data/lib/dynflow/execution_plan.rb +6 -4
- data/lib/dynflow/executors/abstract.rb +12 -0
- data/lib/dynflow/executors/parallel.rb +16 -35
- data/lib/dynflow/executors/parallel/core.rb +13 -8
- data/lib/dynflow/executors/parallel/execution_plan_manager.rb +21 -29
- data/lib/dynflow/executors/parallel/flow_manager.rb +0 -3
- data/lib/dynflow/executors/parallel/running_steps_manager.rb +5 -3
- data/lib/dynflow/executors/parallel/sequential_manager.rb +6 -2
- data/lib/dynflow/executors/parallel/worker.rb +5 -4
- data/lib/dynflow/executors/remote_via_socket.rb +7 -2
- data/lib/dynflow/executors/remote_via_socket/core.rb +66 -32
- data/lib/dynflow/future.rb +1 -1
- data/lib/dynflow/listeners/abstract.rb +4 -0
- data/lib/dynflow/listeners/serialization.rb +42 -8
- data/lib/dynflow/listeners/socket.rb +49 -19
- data/lib/dynflow/middleware.rb +46 -0
- data/lib/dynflow/middleware/action.rb +9 -0
- data/lib/dynflow/middleware/register.rb +32 -0
- data/lib/dynflow/middleware/resolver.rb +63 -0
- data/lib/dynflow/middleware/stack.rb +29 -0
- data/lib/dynflow/middleware/world.rb +58 -0
- data/lib/dynflow/simple_world.rb +1 -0
- data/lib/dynflow/testing/dummy_world.rb +3 -1
- data/lib/dynflow/version.rb +1 -1
- data/lib/dynflow/web_console.rb +7 -2
- data/lib/dynflow/world.rb +29 -9
- data/test/action_test.rb +5 -6
- data/test/execution_plan_test.rb +10 -11
- data/test/executor_test.rb +152 -89
- data/test/middleware_test.rb +109 -0
- data/test/remote_via_socket_test.rb +166 -0
- data/test/{code_workflow_example.rb → support/code_workflow_example.rb} +39 -30
- data/test/support/middleware_example.rb +132 -0
- data/test/test_helper.rb +18 -16
- data/test/testing_test.rb +4 -3
- data/test/web_console_test.rb +1 -2
- metadata +16 -4
data/lib/dynflow/future.rb
CHANGED
@@ -1,15 +1,45 @@
|
|
1
1
|
module Dynflow
|
2
2
|
module Listeners
|
3
3
|
module Serialization
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
module Protocol
|
5
|
+
|
6
|
+
Job = Algebrick.type do
|
7
|
+
Event = type do
|
8
|
+
fields! execution_plan_id: String,
|
9
|
+
step_id: Fixnum,
|
10
|
+
event: Object
|
11
|
+
end
|
12
|
+
|
13
|
+
Execution = type do
|
14
|
+
fields! execution_plan_id: String
|
15
|
+
end
|
16
|
+
|
17
|
+
variants Event, Execution
|
9
18
|
end
|
10
|
-
Done = type { fields request_id: Integer, execution_plan_uuid: String }
|
11
19
|
|
12
|
-
|
20
|
+
Message = Algebrick.type do
|
21
|
+
Request = type do
|
22
|
+
variants Do = type { fields request_id: Integer, job: Job }
|
23
|
+
end
|
24
|
+
|
25
|
+
Response = type do
|
26
|
+
variants Accepted = type { fields request_id: Integer },
|
27
|
+
Failed = type { fields request_id: Integer, error: String },
|
28
|
+
Done = type { fields request_id: Integer }
|
29
|
+
end
|
30
|
+
|
31
|
+
variants Request, Response
|
32
|
+
end
|
33
|
+
|
34
|
+
module Event
|
35
|
+
def to_hash
|
36
|
+
super.update event: Marshal.dump(event)
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.product_from_hash(hash)
|
40
|
+
super(hash.merge 'event' => Marshal.load(hash.fetch('event')))
|
41
|
+
end
|
42
|
+
end
|
13
43
|
end
|
14
44
|
|
15
45
|
def dump(obj)
|
@@ -17,12 +47,16 @@ module Dynflow
|
|
17
47
|
end
|
18
48
|
|
19
49
|
def load(str)
|
20
|
-
|
50
|
+
Protocol::Message.from_hash MultiJson.load(str)
|
21
51
|
end
|
22
52
|
|
23
53
|
def send_message(io, message, barrier = nil)
|
24
54
|
barrier.lock if barrier
|
25
55
|
io.puts dump(message)
|
56
|
+
true
|
57
|
+
rescue SystemCallError => error
|
58
|
+
@logger.warn "message could not be sent #{message} because #{error}"
|
59
|
+
false
|
26
60
|
ensure
|
27
61
|
barrier.unlock if barrier
|
28
62
|
end
|
@@ -5,7 +5,9 @@ module Dynflow
|
|
5
5
|
include Listeners::Serialization
|
6
6
|
include Algebrick::Matching
|
7
7
|
|
8
|
-
|
8
|
+
Terminate = Algebrick.atom
|
9
|
+
|
10
|
+
def initialize(world, socket_path, interval = 1)
|
9
11
|
super(world)
|
10
12
|
|
11
13
|
File.delete socket_path if File.exist? socket_path
|
@@ -14,23 +16,35 @@ module Dynflow
|
|
14
16
|
|
15
17
|
@clients = []
|
16
18
|
@client_barriers = {}
|
17
|
-
@
|
19
|
+
@terminate = false
|
20
|
+
@loop = Thread.new do
|
21
|
+
Thread.current.abort_on_exception = true
|
22
|
+
catch(Terminate) { loop { listen(interval) } }
|
23
|
+
@terminate.resolve true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def terminate(future = Future.new)
|
28
|
+
raise 'multiple calls' if @terminate
|
29
|
+
@terminate = future
|
18
30
|
end
|
19
31
|
|
20
32
|
private
|
21
33
|
|
22
|
-
def listen
|
34
|
+
def listen(interval)
|
35
|
+
shutdown if @terminate
|
36
|
+
|
23
37
|
ios = [@server, *@clients]
|
24
|
-
reads, writes, errors = IO.select(ios, [], ios)
|
25
|
-
reads.each do |readable|
|
38
|
+
reads, writes, errors = IO.select(ios, [], ios, interval)
|
39
|
+
Array(reads).each do |readable|
|
26
40
|
if readable == @server
|
27
41
|
add_client @server.accept
|
28
42
|
logger.info 'Client connected.'
|
29
43
|
|
30
44
|
else
|
31
45
|
match message = receive_message(readable),
|
32
|
-
(on ~
|
33
|
-
|
46
|
+
(on ~Protocol::Do do |(id, job)|
|
47
|
+
execute_job(readable, id, job)
|
34
48
|
end),
|
35
49
|
(on NilClass.to_m do
|
36
50
|
remove_client readable
|
@@ -42,29 +56,36 @@ module Dynflow
|
|
42
56
|
logger.fatal error
|
43
57
|
end
|
44
58
|
|
45
|
-
def
|
59
|
+
def execute_job(readable, id, job)
|
46
60
|
responded = false
|
47
61
|
respond = -> error = nil do
|
48
62
|
unless responded
|
49
63
|
responded = true
|
50
64
|
send_message_to_client(readable, if error
|
51
65
|
logger.error error
|
52
|
-
Failed[id, error.message]
|
66
|
+
Protocol::Failed[id, error.message]
|
53
67
|
else
|
54
|
-
Accepted[id]
|
68
|
+
Protocol::Accepted[id]
|
55
69
|
end)
|
56
70
|
end
|
57
71
|
end
|
58
72
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
73
|
+
future = Future.new.do_then do |_|
|
74
|
+
if future.resolved?
|
75
|
+
respond.call
|
76
|
+
send_message_to_client readable, Protocol::Done[id]
|
77
|
+
else
|
78
|
+
respond.call future.value
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
match job,
|
83
|
+
(on ~Protocol::Execution do |(uuid)|
|
84
|
+
@world.execute(uuid, future)
|
85
|
+
end),
|
86
|
+
(on ~Protocol::Event do |(uuid, step_id, event)|
|
87
|
+
@world.event(uuid, step_id, event, future)
|
88
|
+
end)
|
68
89
|
respond.call
|
69
90
|
rescue Dynflow::Error => e
|
70
91
|
respond.call e
|
@@ -83,6 +104,15 @@ module Dynflow
|
|
83
104
|
def send_message_to_client(client, message)
|
84
105
|
send_message client, message, @client_barriers[client]
|
85
106
|
end
|
107
|
+
|
108
|
+
def shutdown
|
109
|
+
@clients.each { |c| c.shutdown :RDWR }
|
110
|
+
@server.close
|
111
|
+
rescue => e
|
112
|
+
@logger.error e
|
113
|
+
ensure
|
114
|
+
throw Terminate
|
115
|
+
end
|
86
116
|
end
|
87
117
|
end
|
88
118
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Dynflow
|
2
|
+
class Middleware
|
3
|
+
require 'dynflow/middleware/register'
|
4
|
+
require 'dynflow/middleware/world'
|
5
|
+
require 'dynflow/middleware/action'
|
6
|
+
require 'dynflow/middleware/resolver'
|
7
|
+
require 'dynflow/middleware/stack'
|
8
|
+
|
9
|
+
include Algebrick::TypeCheck
|
10
|
+
|
11
|
+
def initialize(stack)
|
12
|
+
@stack = Type! stack, Stack
|
13
|
+
end
|
14
|
+
|
15
|
+
# call `pass` to get deeper with the call
|
16
|
+
def pass(*args)
|
17
|
+
@stack.pass(*args)
|
18
|
+
end
|
19
|
+
|
20
|
+
# to get the action object
|
21
|
+
def action
|
22
|
+
@stack.action or raise "the action is not available"
|
23
|
+
end
|
24
|
+
|
25
|
+
def run(*args)
|
26
|
+
pass(*args)
|
27
|
+
end
|
28
|
+
|
29
|
+
def plan(*args)
|
30
|
+
pass(*args)
|
31
|
+
end
|
32
|
+
|
33
|
+
def finalize(*args)
|
34
|
+
pass(*args)
|
35
|
+
end
|
36
|
+
|
37
|
+
def plan_phase(*args)
|
38
|
+
pass(*args)
|
39
|
+
end
|
40
|
+
|
41
|
+
def finalize_phase(*args)
|
42
|
+
pass(*args)
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Dynflow
|
2
|
+
class Middleware::Register
|
3
|
+
include Algebrick::TypeCheck
|
4
|
+
|
5
|
+
attr_reader :rules
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@rules = Hash.new do |h, k|
|
9
|
+
h[k] = { before: [],
|
10
|
+
after: [],
|
11
|
+
replace: [] }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def use(middleware_class, options = {})
|
16
|
+
unknown_options = options.keys - [:before, :after, :replace]
|
17
|
+
if unknown_options.any?
|
18
|
+
raise ArgumentError, "Unexpected options: #{unknown_options}"
|
19
|
+
end
|
20
|
+
@rules[middleware_class].merge!(options) do |key, old, new|
|
21
|
+
old + Array(new)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def merge!(register)
|
26
|
+
Type! register, Middleware::Register
|
27
|
+
register.rules.each do |klass, rules|
|
28
|
+
use(klass, rules)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'tsort'
|
2
|
+
|
3
|
+
module Dynflow
|
4
|
+
class Middleware::Resolver
|
5
|
+
|
6
|
+
include TSort
|
7
|
+
include Algebrick::TypeCheck
|
8
|
+
|
9
|
+
def initialize(register)
|
10
|
+
@register = Type! register, Middleware::Register
|
11
|
+
end
|
12
|
+
|
13
|
+
def result
|
14
|
+
@result ||= begin
|
15
|
+
@deps = normalize_rules(@register.rules)
|
16
|
+
self.tsort
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
# Takes eliminate :replace and :before rules.
|
23
|
+
# Returns hash, that maps middleware classes to their dependencies
|
24
|
+
def normalize_rules(rules)
|
25
|
+
deps = Hash.new { |h, k| h[k] = [] }
|
26
|
+
substitutions = {}
|
27
|
+
|
28
|
+
# replace before with after on oposite direction and build the
|
29
|
+
# substitutions dictionary
|
30
|
+
rules.each do |middleware_class, middleware_rules|
|
31
|
+
deps[middleware_class].concat(middleware_rules[:after])
|
32
|
+
middleware_rules[:before].each do |dependent_class|
|
33
|
+
deps[dependent_class] << middleware_class
|
34
|
+
end
|
35
|
+
middleware_rules[:replace].each do |replaced|
|
36
|
+
substitutions[replaced] = middleware_class
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# replace the middleware to be substituted
|
41
|
+
substitutions.each do |old, new|
|
42
|
+
deps[new].concat(deps[old])
|
43
|
+
deps.delete(old)
|
44
|
+
end
|
45
|
+
|
46
|
+
# ignore deps, that are not present in the stack
|
47
|
+
deps.each do |middleware_class, middleware_deps|
|
48
|
+
middleware_deps.reject! { |dep| !deps.has_key?(dep) }
|
49
|
+
end
|
50
|
+
|
51
|
+
return deps
|
52
|
+
end
|
53
|
+
|
54
|
+
def tsort_each_node(&block)
|
55
|
+
@deps.each_key(&block)
|
56
|
+
end
|
57
|
+
|
58
|
+
def tsort_each_child(node, &block)
|
59
|
+
@deps.fetch(node).each(&block)
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Dynflow
|
2
|
+
class Middleware::Stack
|
3
|
+
include Algebrick::TypeCheck
|
4
|
+
|
5
|
+
attr_reader :action, :middleware_class, :middleware
|
6
|
+
|
7
|
+
def self.build(middleware_classes, method, action, &block)
|
8
|
+
middleware_classes.reverse_each.reduce(block) do |stack, klass|
|
9
|
+
Middleware::Stack.new(stack, klass, method, action)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(next_stack, middleware_class, method, action)
|
14
|
+
@middleware_class = Child! middleware_class, Middleware
|
15
|
+
@middleware = middleware_class.new self
|
16
|
+
@action = Type! action, Dynflow::Action, NilClass
|
17
|
+
@method = Match! method, :plan, :run, :finalize, :plan_phase, :finalize_phase
|
18
|
+
@next_stack = Type! next_stack, Middleware::Stack, Proc
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(*args)
|
22
|
+
@middleware.send @method, *args
|
23
|
+
end
|
24
|
+
|
25
|
+
def pass(*args)
|
26
|
+
@next_stack.call(*args)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Dynflow
|
2
|
+
class Middleware::World
|
3
|
+
|
4
|
+
include Algebrick::TypeCheck
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@register = Middleware::Register.new
|
8
|
+
clear_cache!
|
9
|
+
end
|
10
|
+
|
11
|
+
def use(*args)
|
12
|
+
clear_cache!
|
13
|
+
@register.use(*args)
|
14
|
+
end
|
15
|
+
|
16
|
+
def execute(method, action_or_class, *args, &block)
|
17
|
+
Match! method, :plan, :run, :finalize, :plan_phase, :finalize_phase
|
18
|
+
if Child? action_or_class, Dynflow::Action
|
19
|
+
action = nil
|
20
|
+
action_class = action_or_class
|
21
|
+
elsif Type? action_or_class, Dynflow::Action
|
22
|
+
action = action_or_class
|
23
|
+
action_class = action.action_class
|
24
|
+
else
|
25
|
+
Algebrick::TypeCheck.error action_or_class, 'is not instance or child class', Dynflow::Action
|
26
|
+
end
|
27
|
+
|
28
|
+
classes = middleware_classes(action_class)
|
29
|
+
stack = Middleware::Stack.build(classes, method, action, &block)
|
30
|
+
stack.call(*args)
|
31
|
+
end
|
32
|
+
|
33
|
+
def clear_cache!
|
34
|
+
@middleware_classes_cache = {}
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def cumulate_register(action_class, register = Middleware::Register.new)
|
40
|
+
register.merge!(@register)
|
41
|
+
unless action_class == Dynflow::Action
|
42
|
+
cumulate_register(action_class.superclass, register)
|
43
|
+
end
|
44
|
+
register.merge!(action_class.middleware)
|
45
|
+
return register
|
46
|
+
end
|
47
|
+
|
48
|
+
def middleware_classes(action_class)
|
49
|
+
unless @middleware_classes_cache.key?(action_class)
|
50
|
+
register = cumulate_register(action_class)
|
51
|
+
resolver = Dynflow::Middleware::Resolver.new(register)
|
52
|
+
@middleware_classes_cache[action_class] = resolver.result
|
53
|
+
end
|
54
|
+
return @middleware_classes_cache[action_class]
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|