dynflow 0.7.9 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,7 @@
1
+ module Dynflow
2
+ module Connectors
3
+ require 'dynflow/connectors/abstract'
4
+ require 'dynflow/connectors/direct'
5
+ require 'dynflow/connectors/database'
6
+ end
7
+ end
@@ -0,0 +1,41 @@
1
+ module Dynflow
2
+ module Connectors
3
+ class Abstract
4
+ include Algebrick::TypeCheck
5
+ include Algebrick::Matching
6
+
7
+ def start_listening(world)
8
+ raise NotImplementedError
9
+ end
10
+
11
+ def stop_listening(world)
12
+ raise NotImplementedError
13
+ end
14
+
15
+ def terminate
16
+ raise NotImplementedError
17
+ end
18
+
19
+ def send(envelope)
20
+ raise NotImplementedError
21
+ end
22
+
23
+ # we need to pass the world, as the connector can be shared
24
+ # between words: we need to know the one to send the message to
25
+ def receive(world, envelope)
26
+ Type! envelope, Dispatcher::Envelope
27
+ match(envelope.message,
28
+ (on Dispatcher::Ping do
29
+ response_envelope = envelope.build_response_envelope(Dispatcher::Pong, world)
30
+ send(response_envelope)
31
+ end),
32
+ (on Dispatcher::Request do
33
+ world.executor_dispatcher.tell([:handle_request, envelope])
34
+ end),
35
+ (on Dispatcher::Response do
36
+ world.client_dispatcher.tell([:dispatch_response, envelope])
37
+ end))
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,175 @@
1
+ module Dynflow
2
+ module Connectors
3
+ class Database < Abstract
4
+
5
+ class PostgresListerner
6
+ def initialize(core, world_id, db)
7
+ @core = core
8
+ @db = db
9
+ @world_id = world_id
10
+ @started = Concurrent::AtomicReference.new
11
+ end
12
+
13
+ def self.notify_supported?(db)
14
+ db.class.name == "Sequel::Postgres::Database"
15
+ end
16
+
17
+ def started?
18
+ @started.get
19
+ end
20
+
21
+ def start
22
+ @started.set true
23
+ @thread = Thread.new do
24
+ @db.listen("world:#{ @world_id }", :loop => true) do
25
+ if started?
26
+ @core << :check_inbox
27
+ else
28
+ break # the listener is stopped: don't continue listening
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ def notify(world_id)
35
+ @db.notify("world:#{world_id}")
36
+ end
37
+
38
+ def stop
39
+ @started.set false
40
+ notify(@world_id)
41
+ end
42
+ end
43
+
44
+ class Core < Actor
45
+ attr_reader :polling_interval
46
+
47
+ def initialize(connector, polling_interval)
48
+ @connector = connector
49
+ @world = nil
50
+ @executor_round_robin = RoundRobin.new
51
+ @stopped = false
52
+ @polling_interval = polling_interval
53
+ end
54
+
55
+ def stopped?
56
+ !!@stopped
57
+ end
58
+
59
+ def start_listening(world)
60
+ @world = world
61
+ @stopped = false
62
+ postgres_listen_start
63
+ self << :periodic_check_inbox
64
+ end
65
+
66
+ def stop_receiving_new_work
67
+ @world.coordinator.deactivate_world(@world.registered_world)
68
+ end
69
+
70
+ def stop_listening
71
+ @stopped = true
72
+ postgres_listen_stop
73
+ end
74
+
75
+ def periodic_check_inbox
76
+ self << :check_inbox
77
+ @world.clock.ping(self, polling_interval, :periodic_check_inbox) unless @stopped
78
+ end
79
+
80
+ def check_inbox
81
+ return unless @world
82
+ receive_envelopes
83
+ end
84
+
85
+ def handle_envelope(envelope)
86
+ world_id = find_receiver(envelope)
87
+ if world_id == @world.id
88
+ if @stopped
89
+ log(Logger::ERROR, "Envelope #{envelope} received for stopped world")
90
+ else
91
+ @connector.receive(@world, envelope)
92
+ end
93
+ else
94
+ send_envelope(update_receiver_id(envelope, world_id))
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ def postgres_listen_start
101
+ if PostgresListerner.notify_supported?(@world.persistence.adapter.db)
102
+ @postgres_listener ||= PostgresListerner.new(self, @world.id, @world.persistence.adapter.db)
103
+ @postgres_listener.start unless @postgres_listener.started?
104
+ end
105
+ end
106
+
107
+ def postgres_listen_stop
108
+ @postgres_listener.stop if @postgres_listener
109
+ end
110
+
111
+ def receive_envelopes
112
+ @world.persistence.pull_envelopes(@world.id).each do |envelope|
113
+ self.tell([:handle_envelope, envelope])
114
+ end
115
+ rescue => e
116
+ log(Logger::ERROR, "Receiving envelopes failed on #{e}")
117
+ end
118
+
119
+ def send_envelope(envelope)
120
+ @world.persistence.push_envelope(envelope)
121
+ if @postgres_listener
122
+ @postgres_listener.notify(envelope.receiver_id)
123
+ end
124
+ rescue => e
125
+ log(Logger::ERROR, "Sending envelope failed on #{e}")
126
+ end
127
+
128
+ def update_receiver_id(envelope, new_receiver_id)
129
+ Dispatcher::Envelope[envelope.request_id, envelope.sender_id, new_receiver_id, envelope.message]
130
+ end
131
+
132
+ def find_receiver(envelope)
133
+ if Dispatcher::AnyExecutor === envelope.receiver_id
134
+ any_executor.id
135
+ else
136
+ envelope.receiver_id
137
+ end
138
+ end
139
+
140
+ def any_executor
141
+ @executor_round_robin.data = @world.coordinator.find_worlds(true)
142
+ @executor_round_robin.next or raise Dynflow::Error, "No executor available"
143
+ end
144
+ end
145
+
146
+ def initialize(world = nil, polling_interval = nil)
147
+ polling_interval ||= begin
148
+ if world && PostgresListerner.notify_supported?(world.persistence.adapter.db)
149
+ 30 # when the notify is supported, we don't need that much polling
150
+ else
151
+ 1
152
+ end
153
+ end
154
+ @core = Core.spawn('connector-database-core', self, polling_interval)
155
+ start_listening(world) if world
156
+ end
157
+
158
+ def start_listening(world)
159
+ @core.ask([:start_listening, world])
160
+ end
161
+
162
+ def stop_receiving_new_work(_)
163
+ @core.ask(:stop_receiving_new_work).wait
164
+ end
165
+
166
+ def stop_listening(_)
167
+ @core.ask(:stop_listening).then { @core.ask(:terminate!) }.wait
168
+ end
169
+
170
+ def send(envelope)
171
+ @core.ask([:handle_envelope, envelope])
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,71 @@
1
+ module Dynflow
2
+ module Connectors
3
+ class Direct < Abstract
4
+
5
+ class Core < Actor
6
+
7
+ def initialize(connector)
8
+ @connector = connector
9
+ @worlds = {}
10
+ @executor_round_robin = RoundRobin.new
11
+ end
12
+
13
+ def start_listening(world)
14
+ @worlds[world.id] = world
15
+ @executor_round_robin.add(world) if world.executor
16
+ end
17
+
18
+ def stop_receiving_new_work(world)
19
+ @executor_round_robin.delete(world)
20
+ end
21
+
22
+ def stop_listening(world)
23
+ @worlds.delete(world.id)
24
+ @executor_round_robin.delete(world) if world.executor
25
+ reference.tell(:terminate!) if @worlds.empty?
26
+ end
27
+
28
+ def handle_envelope(envelope)
29
+ if world = find_receiver(envelope)
30
+ @connector.receive(world, envelope)
31
+ else
32
+ log(Logger::ERROR, "Receiver for envelope #{ envelope } not found")
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def find_receiver(envelope)
39
+ receiver = if Dispatcher::AnyExecutor === envelope.receiver_id
40
+ @executor_round_robin.next
41
+ else
42
+ @worlds[envelope.receiver_id]
43
+ end
44
+ raise Dynflow::Error, "No executor available" unless receiver
45
+ return receiver
46
+ end
47
+ end
48
+
49
+ def initialize(world = nil)
50
+ @core = Core.spawn('connector-direct-core', self)
51
+ start_listening(world) if world
52
+ end
53
+
54
+ def start_listening(world)
55
+ @core.ask([:start_listening, world])
56
+ end
57
+
58
+ def stop_receiving_new_work(world)
59
+ @core.ask([:stop_receiving_new_work, world]).wait
60
+ end
61
+
62
+ def stop_listening(world)
63
+ @core.ask([:stop_listening, world]).wait
64
+ end
65
+
66
+ def send(envelope)
67
+ @core.ask([:handle_envelope, envelope])
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,280 @@
1
+ require 'dynflow/coordinator_adapters'
2
+
3
+ module Dynflow
4
+ class Coordinator
5
+
6
+ include Algebrick::TypeCheck
7
+
8
+ class DuplicateRecordError < Dynflow::Error
9
+ attr_reader :record
10
+
11
+ def initialize(record)
12
+ @record = record
13
+ super("record #{record} already exists")
14
+ end
15
+ end
16
+
17
+ class LockError < Dynflow::Error
18
+ attr_reader :lock
19
+
20
+ def initialize(lock)
21
+ @lock = lock
22
+ super("Unable to acquire lock #{lock}")
23
+ end
24
+ end
25
+
26
+ class Record < Serializable
27
+ attr_reader :data
28
+
29
+ include Algebrick::TypeCheck
30
+
31
+ def self.new_from_hash(hash)
32
+ self.allocate.tap { |record| record.from_hash(hash) }
33
+ end
34
+
35
+ def self.constantize(name)
36
+ Serializable.constantize(name)
37
+ rescue NameError
38
+ # If we don't find the lock name, return the most generic version
39
+ Record
40
+ end
41
+
42
+ def initialize(*args)
43
+ @data ||= {}
44
+ @data = @data.merge(class: self.class.name).with_indifferent_access
45
+ end
46
+
47
+ def from_hash(hash)
48
+ @data = hash
49
+ @from_hash = true
50
+ end
51
+
52
+ def to_hash
53
+ @data
54
+ end
55
+
56
+ def id
57
+ @data[:id]
58
+ end
59
+
60
+ # @api override
61
+ # check to be performed before we try to acquire the lock
62
+ def validate!
63
+ Type! id, String
64
+ Type! @data, Hash
65
+ end
66
+
67
+ def to_s
68
+ "#{self.class.name}: #{id}"
69
+ end
70
+
71
+ def ==(other_object)
72
+ self.class == other_object.class && self.id == other_object.id
73
+ end
74
+
75
+ def hash
76
+ [self.class, self.id].hash
77
+ end
78
+ end
79
+
80
+ class WorldRecord < Record
81
+ def initialize(world)
82
+ super
83
+ @data[:id] = world.id
84
+ @data[:meta] = world.meta
85
+ end
86
+
87
+ def meta
88
+ @data[:meta]
89
+ end
90
+ end
91
+
92
+ class ExecutorWorld < WorldRecord
93
+ def initialize(world)
94
+ super
95
+ self.active = !world.terminating?
96
+ end
97
+
98
+ def active?
99
+ @data[:active]
100
+ end
101
+
102
+ def active=(value)
103
+ Type! value, Algebrick::Types::Boolean
104
+ @data[:active] = value
105
+ end
106
+ end
107
+
108
+ class ClientWorld < WorldRecord
109
+ end
110
+
111
+ class Lock < Record
112
+ def self.constantize(name)
113
+ Serializable.constantize(name)
114
+ rescue NameError
115
+ # If we don't find the lock name, return the most generic version
116
+ Lock
117
+ end
118
+
119
+ def to_s
120
+ "#{self.class.name}: #{id} by #{owner_id}"
121
+ end
122
+
123
+ def owner_id
124
+ @data[:owner_id]
125
+ end
126
+
127
+ # @api override
128
+ # check to be performed before we try to acquire the lock
129
+ def validate!
130
+ super
131
+ raise "Can't acquire the lock after deserialization" if @from_hash
132
+ Type! owner_id, String
133
+ end
134
+
135
+ def to_s
136
+ "#{self.class.name}: #{id} by #{owner_id}"
137
+ end
138
+ end
139
+
140
+ class LockByWorld < Lock
141
+ def initialize(world)
142
+ super
143
+ @world = world
144
+ @data.merge!(owner_id: "world:#{world.id}", world_id: world.id)
145
+ end
146
+
147
+ def validate!
148
+ super
149
+ raise Errors::InactiveWorldError.new(@world) if @world.terminating?
150
+ end
151
+
152
+ def world_id
153
+ @data[:world_id]
154
+ end
155
+
156
+ end
157
+
158
+ class WorldInvalidationLock < LockByWorld
159
+ def initialize(world, invalidated_world)
160
+ super(world)
161
+ @data[:id] = "world-invalidation:#{invalidated_world.id}"
162
+ end
163
+ end
164
+
165
+ class AutoExecuteLock < LockByWorld
166
+ def initialize(*args)
167
+ super
168
+ @data[:id] = "auto-execute"
169
+ end
170
+ end
171
+
172
+ class ExecutionLock < LockByWorld
173
+ def initialize(world, execution_plan_id, client_world_id, request_id)
174
+ super(world)
175
+ @data.merge!(id: "execution-plan:#{execution_plan_id}",
176
+ execution_plan_id: execution_plan_id,
177
+ client_world_id: client_world_id,
178
+ request_id: request_id)
179
+ end
180
+
181
+ # we need to store the following data in case of
182
+ # invalidation of the lock from outside (after
183
+ # the owner world terminated unexpectedly)
184
+ def execution_plan_id
185
+ @data[:execution_plan_id]
186
+ end
187
+
188
+ def client_world_id
189
+ @data[:client_world_id]
190
+ end
191
+
192
+ def request_id
193
+ @data[:request_id]
194
+ end
195
+ end
196
+
197
+ attr_reader :adapter
198
+
199
+ def initialize(coordinator_adapter)
200
+ @adapter = coordinator_adapter
201
+ end
202
+
203
+ def acquire(lock, &block)
204
+ Type! lock, Lock
205
+ lock.validate!
206
+ adapter.create_record(lock)
207
+ if block
208
+ begin
209
+ block.call
210
+ ensure
211
+ release(lock)
212
+ end
213
+ end
214
+ rescue DuplicateRecordError => e
215
+ raise LockError.new(e.record)
216
+ end
217
+
218
+ def release(lock)
219
+ Type! lock, Lock
220
+ adapter.delete_record(lock)
221
+ end
222
+
223
+ def release_by_owner(owner_id)
224
+ find_locks(owner_id: owner_id).map { |lock| release(lock) }
225
+ end
226
+
227
+ def find_locks(filter_options)
228
+ adapter.find_records(filter_options).map do |lock_data|
229
+ Lock.from_hash(lock_data)
230
+ end
231
+ end
232
+
233
+ def create_record(record)
234
+ Type! record, Record
235
+ adapter.create_record(record)
236
+ end
237
+
238
+ def update_record(record)
239
+ Type! record, Record
240
+ adapter.update_record(record)
241
+ end
242
+
243
+ def delete_record(record)
244
+ Type! record, Record
245
+ adapter.delete_record(record)
246
+ end
247
+
248
+ def find_records(filter)
249
+ adapter.find_records(filter).map do |record_data|
250
+ Record.from_hash(record_data)
251
+ end
252
+ end
253
+
254
+ def find_worlds(active_executor_only = false, filters = {})
255
+ ret = find_records(filters.merge(class: Coordinator::ExecutorWorld.name))
256
+ if active_executor_only
257
+ ret = ret.select(&:active?)
258
+ else
259
+ ret.concat(find_records(filters.merge(class: Coordinator::ClientWorld.name)))
260
+ end
261
+ ret
262
+ end
263
+
264
+ def register_world(world)
265
+ Type! world, Coordinator::ClientWorld, Coordinator::ExecutorWorld
266
+ create_record(world)
267
+ end
268
+
269
+ def delete_world(world)
270
+ Type! world, Coordinator::ClientWorld, Coordinator::ExecutorWorld
271
+ delete_record(world)
272
+ end
273
+
274
+ def deactivate_world(world)
275
+ Type! world, Coordinator::ExecutorWorld
276
+ world.active = false
277
+ update_record(world)
278
+ end
279
+ end
280
+ end