dynflow 0.7.9 → 0.8.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 (118) hide show
  1. data/.gitignore +2 -0
  2. data/.travis.yml +16 -1
  3. data/Gemfile +13 -1
  4. data/doc/pages/source/_drafts/2015-03-01-new-documentation.markdown +10 -0
  5. data/doc/pages/source/_includes/menu.html +1 -0
  6. data/doc/pages/source/_includes/menu_right.html +1 -1
  7. data/doc/pages/source/_sass/_bootstrap-variables.sass +1 -0
  8. data/doc/pages/source/_sass/_style.scss +4 -0
  9. data/doc/pages/source/blog/index.html +12 -0
  10. data/doc/pages/source/documentation/index.md +330 -5
  11. data/dynflow.gemspec +3 -1
  12. data/examples/example_helper.rb +18 -11
  13. data/examples/orchestrate_evented.rb +2 -1
  14. data/examples/remote_executor.rb +53 -20
  15. data/lib/dynflow.rb +16 -6
  16. data/lib/dynflow/action/suspended.rb +1 -1
  17. data/lib/dynflow/action/with_sub_plans.rb +3 -6
  18. data/lib/dynflow/actor.rb +56 -0
  19. data/lib/dynflow/clock.rb +43 -38
  20. data/lib/dynflow/config.rb +107 -0
  21. data/lib/dynflow/connectors.rb +7 -0
  22. data/lib/dynflow/connectors/abstract.rb +41 -0
  23. data/lib/dynflow/connectors/database.rb +175 -0
  24. data/lib/dynflow/connectors/direct.rb +71 -0
  25. data/lib/dynflow/coordinator.rb +280 -0
  26. data/lib/dynflow/coordinator_adapters.rb +8 -0
  27. data/lib/dynflow/coordinator_adapters/abstract.rb +28 -0
  28. data/lib/dynflow/coordinator_adapters/sequel.rb +29 -0
  29. data/lib/dynflow/dispatcher.rb +58 -0
  30. data/lib/dynflow/dispatcher/abstract.rb +14 -0
  31. data/lib/dynflow/dispatcher/client_dispatcher.rb +139 -0
  32. data/lib/dynflow/dispatcher/executor_dispatcher.rb +86 -0
  33. data/lib/dynflow/errors.rb +7 -1
  34. data/lib/dynflow/execution_history.rb +46 -0
  35. data/lib/dynflow/execution_plan.rb +19 -15
  36. data/lib/dynflow/executors.rb +0 -1
  37. data/lib/dynflow/executors/abstract.rb +5 -10
  38. data/lib/dynflow/executors/parallel.rb +16 -13
  39. data/lib/dynflow/executors/parallel/core.rb +76 -78
  40. data/lib/dynflow/executors/parallel/execution_plan_manager.rb +4 -5
  41. data/lib/dynflow/executors/parallel/pool.rb +22 -52
  42. data/lib/dynflow/executors/parallel/running_steps_manager.rb +9 -2
  43. data/lib/dynflow/executors/parallel/worker.rb +5 -10
  44. data/lib/dynflow/persistence.rb +14 -0
  45. data/lib/dynflow/persistence_adapters/abstract.rb +14 -3
  46. data/lib/dynflow/persistence_adapters/sequel.rb +142 -38
  47. data/lib/dynflow/persistence_adapters/sequel_migrations/004_coordinator_records.rb +14 -0
  48. data/lib/dynflow/persistence_adapters/sequel_migrations/005_envelopes.rb +14 -0
  49. data/lib/dynflow/round_robin.rb +37 -0
  50. data/lib/dynflow/serializable.rb +1 -2
  51. data/lib/dynflow/serializer.rb +46 -0
  52. data/lib/dynflow/testing/dummy_executor.rb +2 -2
  53. data/lib/dynflow/testing/dummy_world.rb +1 -1
  54. data/lib/dynflow/transaction_adapters/abstract.rb +0 -5
  55. data/lib/dynflow/transaction_adapters/active_record.rb +0 -10
  56. data/lib/dynflow/version.rb +1 -1
  57. data/lib/dynflow/web.rb +26 -0
  58. data/lib/dynflow/web/console.rb +108 -0
  59. data/lib/dynflow/web/console_helpers.rb +158 -0
  60. data/lib/dynflow/web/filtering_helpers.rb +85 -0
  61. data/lib/dynflow/web/world_helpers.rb +9 -0
  62. data/lib/dynflow/web_console.rb +3 -310
  63. data/lib/dynflow/world.rb +188 -119
  64. data/test/abnormal_states_recovery_test.rb +152 -0
  65. data/test/action_test.rb +2 -3
  66. data/test/clock_test.rb +1 -5
  67. data/test/coordinator_test.rb +152 -0
  68. data/test/dispatcher_test.rb +146 -0
  69. data/test/execution_plan_test.rb +2 -1
  70. data/test/executor_test.rb +534 -612
  71. data/test/middleware_test.rb +4 -4
  72. data/test/persistence_test.rb +17 -0
  73. data/test/prepare_travis_env.sh +35 -0
  74. data/test/rescue_test.rb +5 -3
  75. data/test/round_robin_test.rb +28 -0
  76. data/test/support/code_workflow_example.rb +0 -73
  77. data/test/support/dummy_example.rb +130 -0
  78. data/test/support/test_execution_log.rb +41 -0
  79. data/test/test_helper.rb +222 -116
  80. data/test/testing_test.rb +10 -10
  81. data/test/web_console_test.rb +3 -3
  82. data/test/world_test.rb +23 -0
  83. data/web/assets/images/logo-square.png +0 -0
  84. data/web/assets/stylesheets/application.css +9 -0
  85. data/web/assets/vendor/bootstrap/config.json +429 -0
  86. data/web/assets/vendor/bootstrap/css/bootstrap-theme.css +479 -0
  87. data/web/assets/vendor/bootstrap/css/bootstrap-theme.min.css +10 -0
  88. data/web/assets/vendor/bootstrap/css/bootstrap.css +5377 -4980
  89. data/web/assets/vendor/bootstrap/css/bootstrap.min.css +9 -8
  90. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.eot +0 -0
  91. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.svg +288 -0
  92. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf +0 -0
  93. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff +0 -0
  94. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff2 +0 -0
  95. data/web/assets/vendor/bootstrap/js/bootstrap.js +1674 -1645
  96. data/web/assets/vendor/bootstrap/js/bootstrap.min.js +11 -5
  97. data/web/views/execution_history.erb +17 -0
  98. data/web/views/index.erb +4 -6
  99. data/web/views/layout.erb +44 -8
  100. data/web/views/show.erb +4 -5
  101. data/web/views/worlds.erb +26 -0
  102. metadata +116 -23
  103. checksums.yaml +0 -15
  104. data/lib/dynflow/daemon.rb +0 -30
  105. data/lib/dynflow/executors/remote_via_socket.rb +0 -43
  106. data/lib/dynflow/executors/remote_via_socket/core.rb +0 -184
  107. data/lib/dynflow/future.rb +0 -173
  108. data/lib/dynflow/listeners.rb +0 -7
  109. data/lib/dynflow/listeners/abstract.rb +0 -17
  110. data/lib/dynflow/listeners/serialization.rb +0 -77
  111. data/lib/dynflow/listeners/socket.rb +0 -117
  112. data/lib/dynflow/micro_actor.rb +0 -102
  113. data/lib/dynflow/simple_world.rb +0 -19
  114. data/test/remote_via_socket_test.rb +0 -170
  115. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.css +0 -1109
  116. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.min.css +0 -9
  117. data/web/assets/vendor/bootstrap/img/glyphicons-halflings-white.png +0 -0
  118. data/web/assets/vendor/bootstrap/img/glyphicons-halflings.png +0 -0
@@ -1,7 +0,0 @@
1
- module Dynflow
2
- module Listeners
3
- require 'dynflow/listeners/serialization'
4
- require 'dynflow/listeners/abstract'
5
- require 'dynflow/listeners/socket'
6
- end
7
- end
@@ -1,17 +0,0 @@
1
- module Dynflow
2
- module Listeners
3
- class Abstract
4
- include Algebrick::TypeCheck
5
- attr_reader :world, :logger
6
-
7
- def initialize(world)
8
- @world = Type! world, World
9
- @logger = world.logger
10
- end
11
-
12
- def terminate(future = Future.new)
13
- raise NotImplementedError
14
- end
15
- end
16
- end
17
- end
@@ -1,77 +0,0 @@
1
- module Dynflow
2
- module Listeners
3
- module Serialization
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
18
- end
19
-
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
- # TODO fix the workaround
36
- # marshal and then use base64 because not all json libs can correctly escape binary data
37
- def to_hash
38
- super.update event: Base64.strict_encode64(Marshal.dump(event))
39
- end
40
-
41
- def self.product_from_hash(hash)
42
- super(hash.merge 'event' => Marshal.load(Base64.strict_decode64(hash.fetch('event'))))
43
- end
44
- end
45
- end
46
-
47
- def dump(obj)
48
- MultiJson.dump(obj.to_hash)
49
- end
50
-
51
- def load(str)
52
- Protocol::Message.from_hash MultiJson.load(str)
53
- end
54
-
55
- def send_message(io, message, barrier = nil)
56
- barrier.lock if barrier
57
- io.puts dump(message)
58
- true
59
- rescue SystemCallError => error
60
- @logger.warn "message could not be sent #{message} because #{error}"
61
- false
62
- ensure
63
- barrier.unlock if barrier
64
- end
65
-
66
- def receive_message(io)
67
- if (message = io.gets)
68
- load(message)
69
- else
70
- nil
71
- end
72
- rescue IOError
73
- nil
74
- end
75
- end
76
- end
77
- end
@@ -1,117 +0,0 @@
1
- module Dynflow
2
- module Listeners
3
- class Socket < Abstract
4
- include Listeners::Serialization
5
- include Algebrick::Matching
6
-
7
- Terminate = Algebrick.atom
8
-
9
- def initialize(world, socket_path, interval = 1)
10
- super(world)
11
-
12
- File.delete socket_path if File.exist? socket_path
13
- @server = UNIXServer.new socket_path
14
- File.chmod(0600, socket_path)
15
-
16
- @clients = []
17
- @client_barriers = {}
18
- @terminate = false
19
- @loop = Thread.new do
20
- Thread.current.abort_on_exception = true
21
- catch(Terminate) { loop { listen(interval) } }
22
- @terminate.resolve true
23
- end
24
- end
25
-
26
- def terminate(future = Future.new)
27
- raise 'multiple calls' if @terminate
28
- @terminate = future
29
- end
30
-
31
- private
32
-
33
- def listen(interval)
34
- shutdown if @terminate
35
-
36
- ios = [@server, *@clients]
37
- reads, writes, errors = IO.select(ios, [], ios, interval)
38
- Array(reads).each do |readable|
39
- if readable == @server
40
- add_client @server.accept
41
- logger.info 'Client connected.'
42
-
43
- else
44
- match message = receive_message(readable),
45
- (on ~Protocol::Do do |(id, job)|
46
- execute_job(readable, id, job)
47
- end),
48
- (on NilClass.to_m do
49
- remove_client readable
50
- logger.info 'Client disconnected.'
51
- end)
52
- end
53
- end
54
- rescue => error
55
- logger.fatal error
56
- end
57
-
58
- def execute_job(readable, id, job)
59
- responded = false
60
- respond = -> error = nil do
61
- unless responded
62
- responded = true
63
- send_message_to_client(readable, if error
64
- logger.error error
65
- Protocol::Failed[id, error.message]
66
- else
67
- Protocol::Accepted[id]
68
- end)
69
- end
70
- end
71
-
72
- future = Future.new.do_then do |_|
73
- if future.resolved?
74
- respond.call
75
- send_message_to_client readable, Protocol::Done[id]
76
- else
77
- respond.call future.value
78
- end
79
- end
80
-
81
- match job,
82
- (on ~Protocol::Execution do |(uuid)|
83
- @world.execute(uuid, future)
84
- end),
85
- (on ~Protocol::Event do |(uuid, step_id, event)|
86
- @world.event(uuid, step_id, event, future)
87
- end)
88
- respond.call
89
- rescue Dynflow::Error => e
90
- respond.call e
91
- end
92
-
93
- def add_client(client)
94
- @clients << client
95
- @client_barriers[client] = Mutex.new
96
- end
97
-
98
- def remove_client(client)
99
- @clients.delete client
100
- @client_barriers.delete client
101
- end
102
-
103
- def send_message_to_client(client, message)
104
- send_message client, message, @client_barriers[client]
105
- end
106
-
107
- def shutdown
108
- @clients.each { |c| c.shutdown :RDWR }
109
- @server.close
110
- rescue => e
111
- @logger.error e
112
- ensure
113
- throw Terminate
114
- end
115
- end
116
- end
117
- end
@@ -1,102 +0,0 @@
1
- module Dynflow
2
- class MicroActor
3
- include Algebrick::TypeCheck
4
- include Algebrick::Matching
5
-
6
- attr_reader :logger, :initialized
7
-
8
- Terminate = Algebrick.atom
9
-
10
- def initialize(logger, *args)
11
- @logger = logger
12
- @initialized = Future.new
13
- @thread = Thread.new { run *args }
14
- Thread.pass until @mailbox
15
- end
16
-
17
- def <<(message)
18
- raise 'actor terminated' if terminated?
19
- @mailbox << [message, nil]
20
- self
21
- end
22
-
23
- def ask(message, future = Future.new)
24
- future.fail Dynflow::Error.new('actor terminated') if terminated?
25
- @mailbox << [message, future]
26
- future
27
- end
28
-
29
- def stopped?
30
- @terminated.ready?
31
- end
32
-
33
- private
34
-
35
- def delayed_initialize(*args)
36
- end
37
-
38
- def termination
39
- terminate!
40
- end
41
-
42
- def terminating?
43
- @terminated
44
- end
45
-
46
- def terminated?
47
- terminating? && @terminated.ready?
48
- end
49
-
50
- def terminate!
51
- raise unless Thread.current == @thread
52
- @terminated.resolve true
53
- throw Terminate
54
- end
55
-
56
- def on_message(message)
57
- raise NotImplementedError
58
- end
59
-
60
- def receive
61
- message, future = @mailbox.pop
62
- #logger.debug "#{self.class} received:\n #{message}"
63
- if message == Terminate
64
- # TODO do not use this future to store in @terminated use one added to Terminate message
65
- if terminating?
66
- @terminated.do_then { future.resolve true } if future
67
- else
68
- @terminated = (future || Future.new)
69
- termination
70
- end
71
- else
72
- on_envelope message, future
73
- end
74
- rescue => error
75
- logger.fatal error
76
- end
77
-
78
- def on_envelope(message, future)
79
- if future
80
- future.evaluate_to { on_message message }
81
- else
82
- on_message message
83
- end
84
- if future && future.failed?
85
- logger.error future.value
86
- end
87
- end
88
-
89
- def run(*args)
90
- Thread.current.abort_on_exception = true
91
-
92
- @mailbox = Queue.new
93
- @terminated = nil
94
-
95
- delayed_initialize(*args)
96
- Thread.pass until @initialized
97
- @initialized.resolve true
98
-
99
- catch(Terminate) { loop { receive } }
100
- end
101
- end
102
- end
@@ -1,19 +0,0 @@
1
- module Dynflow
2
- class SimpleWorld < World
3
- def initialize(options_hash = {})
4
- super options_hash
5
- at_exit { self.terminate.wait } if options[:auto_terminate]
6
- # we can check consistency here because SimpleWorld doesn't expect
7
- # remote executor being in place.
8
- self.consistency_check
9
- self.execute_planned_execution_plans
10
- end
11
-
12
- def default_options
13
- super.merge(pool_size: 5,
14
- persistence_adapter: PersistenceAdapters::Sequel.new('sqlite:/'),
15
- transaction_adapter: TransactionAdapters::None.new,
16
- auto_terminate: true)
17
- end
18
- end
19
- end
@@ -1,170 +0,0 @@
1
- require_relative 'test_helper'
2
-
3
- describe 'remote communication' do
4
-
5
- let(:persistence_adapter) { Dynflow::PersistenceAdapters::Sequel.new('sqlite:/') }
6
-
7
- module Helpers
8
- def socket_path
9
- @socket_path ||= Dir.tmpdir + "/dynflow_remote_#{rand(1e30)}"
10
- end
11
-
12
- def logger_adapter
13
- WorldInstance.logger_adapter
14
- end
15
-
16
- def create_world
17
- Dynflow::SimpleWorld.new logger_adapter: logger_adapter,
18
- auto_terminate: false,
19
- exit_on_terminate: false,
20
- persistence_adapter: persistence_adapter
21
- end
22
-
23
- def create_remote_world
24
- Dynflow::SimpleWorld.new(
25
- logger_adapter: logger_adapter,
26
- auto_terminate: false,
27
- persistence_adapter: persistence_adapter,
28
- exit_on_terminate: false,
29
- executor: -> remote_world do
30
- Dynflow::Executors::RemoteViaSocket.new(remote_world, socket_path)
31
- end)
32
- end
33
-
34
- def create_listener(world)
35
- Dynflow::Listeners::Socket.new world, socket_path, 0.05
36
- end
37
-
38
- def terminate(*terminable)
39
- terminable.each { |t| t.terminate.wait }
40
- end
41
- end
42
-
43
- include Helpers
44
-
45
- it 'raises when not connected' do
46
- remote_world = create_remote_world
47
- result = remote_world.trigger Support::CodeWorkflowExample::Commit, 'sha'
48
- result.must_be :planned?
49
- result.wont_be :triggered?
50
- result.error.must_be_kind_of Dynflow::Error
51
-
52
- terminate remote_world
53
- end
54
-
55
- describe 'execute_planned_execution_plans' do
56
- specify do
57
- remote_world = create_remote_world
58
- result = remote_world.trigger Support::CodeWorkflowExample::Commit, 'sha'
59
- result.must_be :planned?
60
- result.wont_be :triggered?
61
- result.error.must_be_kind_of Dynflow::Error
62
-
63
- remote_world.persistence.load_execution_plan(result.id).state.must_equal :planned
64
-
65
- world = create_world
66
- listener = create_listener(world)
67
-
68
- # waiting until it starts executing
69
- assert(10.times do |i|
70
- state = world.persistence.load_execution_plan(result.id).state
71
- break :ok if [:running, :stopped].include? state
72
- puts 'retry'
73
- sleep 0.01 * i
74
- end == :ok)
75
-
76
- terminate remote_world, listener, world
77
- end
78
- end
79
-
80
- describe 'shutting down' do
81
- [:remote_world, :world, :listener].permutation.each do |order|
82
- it "works when in order #{order}" do
83
- objects = { world: w = create_world,
84
- listener: create_listener(w),
85
- remote_world: remote_world = create_remote_world }
86
-
87
- result = remote_world.trigger Support::CodeWorkflowExample::Commit, 'sha'
88
- result.must_be :planned?
89
- result.finished.value!.must_be_kind_of Dynflow::ExecutionPlan
90
-
91
- terminate *objects.values_at(*order)
92
- assert true, 'it has to reach this'
93
- end
94
- end
95
-
96
- it 'allows to work others' do
97
- world = create_world
98
- listener = create_listener(world)
99
- rmw1 = create_remote_world
100
- rmw2 = create_remote_world
101
-
102
- [rmw1.trigger(Support::CodeWorkflowExample::Commit, 'sha').finished,
103
- rmw2.trigger(Support::CodeWorkflowExample::Commit, 'sha').finished].
104
- each(&:value!)
105
-
106
- terminate rmw1
107
-
108
- refute rmw1.trigger(Support::CodeWorkflowExample::Commit, 'sha').triggered?
109
- rmw2.trigger(Support::CodeWorkflowExample::Commit, 'sha').
110
- finished.value!.must_be_kind_of Dynflow::ExecutionPlan
111
-
112
- terminate rmw2, listener, world
113
- end
114
-
115
- it 'raises when disconnected while executing' do
116
- world = create_world
117
- listener = create_listener(world)
118
- remote_world = create_remote_world
119
-
120
- result = remote_world.trigger(Support::CodeWorkflowExample::Slow, 2)
121
- result.must_be :planned?
122
-
123
- terminate listener
124
-
125
- -> { result.finished.value! }.must_raise Dynflow::Future::FutureFailed
126
- terminate remote_world, world
127
- end
128
-
129
- end
130
-
131
- it 'restarts' do
132
- world = create_world
133
- listener = create_listener(world)
134
- remote_world = create_remote_world
135
-
136
- remote_world.trigger(Support::CodeWorkflowExample::Commit, 'sha').finished.value!
137
-
138
- terminate listener
139
- Thread.pass while remote_world.executor.connected?
140
- listener = create_listener world
141
- Thread.pass until remote_world.executor.connected?
142
-
143
- remote_world.trigger(Support::CodeWorkflowExample::Commit, 'sha').finished.value!
144
-
145
- terminate listener, world
146
- Thread.pass while remote_world.executor.connected?
147
- world = create_world
148
- listener = create_listener world
149
- Thread.pass until remote_world.executor.connected?
150
-
151
- remote_world.trigger(Support::CodeWorkflowExample::Commit, 'sha').finished.value!
152
-
153
- terminate listener, world, remote_world
154
- end
155
-
156
- describe '#connected?' do
157
- specify do
158
- remote_world = create_remote_world
159
-
160
- remote_world.executor.connected?.must_equal false
161
-
162
- world = create_world
163
- listener = create_listener world
164
-
165
- remote_world.executor.connected?.must_equal true
166
-
167
- terminate listener, world, remote_world
168
- end
169
- end
170
- end