dynflow 0.2.0 → 0.3.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 (44) hide show
  1. data/.travis.yml +1 -0
  2. data/lib/dynflow.rb +1 -0
  3. data/lib/dynflow/action.rb +5 -3
  4. data/lib/dynflow/action/finalize_phase.rb +3 -1
  5. data/lib/dynflow/action/plan_phase.rb +4 -2
  6. data/lib/dynflow/action/run_phase.rb +3 -1
  7. data/lib/dynflow/daemon.rb +1 -0
  8. data/lib/dynflow/execution_plan.rb +6 -4
  9. data/lib/dynflow/executors/abstract.rb +12 -0
  10. data/lib/dynflow/executors/parallel.rb +16 -35
  11. data/lib/dynflow/executors/parallel/core.rb +13 -8
  12. data/lib/dynflow/executors/parallel/execution_plan_manager.rb +21 -29
  13. data/lib/dynflow/executors/parallel/flow_manager.rb +0 -3
  14. data/lib/dynflow/executors/parallel/running_steps_manager.rb +5 -3
  15. data/lib/dynflow/executors/parallel/sequential_manager.rb +6 -2
  16. data/lib/dynflow/executors/parallel/worker.rb +5 -4
  17. data/lib/dynflow/executors/remote_via_socket.rb +7 -2
  18. data/lib/dynflow/executors/remote_via_socket/core.rb +66 -32
  19. data/lib/dynflow/future.rb +1 -1
  20. data/lib/dynflow/listeners/abstract.rb +4 -0
  21. data/lib/dynflow/listeners/serialization.rb +42 -8
  22. data/lib/dynflow/listeners/socket.rb +49 -19
  23. data/lib/dynflow/middleware.rb +46 -0
  24. data/lib/dynflow/middleware/action.rb +9 -0
  25. data/lib/dynflow/middleware/register.rb +32 -0
  26. data/lib/dynflow/middleware/resolver.rb +63 -0
  27. data/lib/dynflow/middleware/stack.rb +29 -0
  28. data/lib/dynflow/middleware/world.rb +58 -0
  29. data/lib/dynflow/simple_world.rb +1 -0
  30. data/lib/dynflow/testing/dummy_world.rb +3 -1
  31. data/lib/dynflow/version.rb +1 -1
  32. data/lib/dynflow/web_console.rb +7 -2
  33. data/lib/dynflow/world.rb +29 -9
  34. data/test/action_test.rb +5 -6
  35. data/test/execution_plan_test.rb +10 -11
  36. data/test/executor_test.rb +152 -89
  37. data/test/middleware_test.rb +109 -0
  38. data/test/remote_via_socket_test.rb +166 -0
  39. data/test/{code_workflow_example.rb → support/code_workflow_example.rb} +39 -30
  40. data/test/support/middleware_example.rb +132 -0
  41. data/test/test_helper.rb +18 -16
  42. data/test/testing_test.rb +4 -3
  43. data/test/web_console_test.rb +1 -2
  44. metadata +16 -4
@@ -1,6 +1,6 @@
1
1
  module Dynflow
2
2
  class Future
3
- Error = Class.new StandardError
3
+ Error = Class.new Dynflow::Error
4
4
  FutureAlreadySet = Class.new Error
5
5
  FutureFailed = Class.new Error
6
6
  TimeOut = Class.new Error
@@ -8,6 +8,10 @@ module Dynflow
8
8
  @world = Type! world, World
9
9
  @logger = world.logger
10
10
  end
11
+
12
+ def terminate(future = Future.new)
13
+ raise NotImplementedError
14
+ end
11
15
  end
12
16
  end
13
17
  end
@@ -1,15 +1,45 @@
1
1
  module Dynflow
2
2
  module Listeners
3
3
  module Serialization
4
- SocketMessage = Algebrick.type do
5
- Execute = type { fields request_id: Integer, execution_plan_uuid: String }
6
- Confirmation = type do
7
- variants Accepted = type { fields request_id: Integer },
8
- Failed = type { fields request_id: Integer, error: String }
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
- variants Execute, Confirmation, Done
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
- SocketMessage.from_hash MultiJson.load(str)
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
- def initialize(world, socket_path)
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
- @loop = Thread.new { loop { listen } }
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 ~Execute do |(id, uuid)|
33
- execute(readable, id, uuid)
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 execute(readable, id, uuid)
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
- @world.execute(uuid,
60
- f = Future.new do |_|
61
- if f.resolved?
62
- respond.call
63
- send_message_to_client readable, Done[id, uuid]
64
- else
65
- respond.call f.value
66
- end
67
- end)
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,9 @@
1
+ module Dynflow
2
+ module Middleware::Action
3
+
4
+ def middleware
5
+ @middleware ||= Middleware::Register.new
6
+ end
7
+
8
+ end
9
+ 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