dynflow 0.7.9 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/.travis.yml +16 -1
- data/Gemfile +13 -1
- data/doc/pages/source/_drafts/2015-03-01-new-documentation.markdown +10 -0
- data/doc/pages/source/_includes/menu.html +1 -0
- data/doc/pages/source/_includes/menu_right.html +1 -1
- data/doc/pages/source/_sass/_bootstrap-variables.sass +1 -0
- data/doc/pages/source/_sass/_style.scss +4 -0
- data/doc/pages/source/blog/index.html +12 -0
- data/doc/pages/source/documentation/index.md +330 -5
- data/dynflow.gemspec +3 -1
- data/examples/example_helper.rb +18 -11
- data/examples/orchestrate_evented.rb +2 -1
- data/examples/remote_executor.rb +53 -20
- data/lib/dynflow.rb +16 -6
- data/lib/dynflow/action/suspended.rb +1 -1
- data/lib/dynflow/action/with_sub_plans.rb +3 -6
- data/lib/dynflow/actor.rb +56 -0
- data/lib/dynflow/clock.rb +43 -38
- data/lib/dynflow/config.rb +107 -0
- data/lib/dynflow/connectors.rb +7 -0
- data/lib/dynflow/connectors/abstract.rb +41 -0
- data/lib/dynflow/connectors/database.rb +175 -0
- data/lib/dynflow/connectors/direct.rb +71 -0
- data/lib/dynflow/coordinator.rb +280 -0
- data/lib/dynflow/coordinator_adapters.rb +8 -0
- data/lib/dynflow/coordinator_adapters/abstract.rb +28 -0
- data/lib/dynflow/coordinator_adapters/sequel.rb +29 -0
- data/lib/dynflow/dispatcher.rb +58 -0
- data/lib/dynflow/dispatcher/abstract.rb +14 -0
- data/lib/dynflow/dispatcher/client_dispatcher.rb +139 -0
- data/lib/dynflow/dispatcher/executor_dispatcher.rb +86 -0
- data/lib/dynflow/errors.rb +7 -1
- data/lib/dynflow/execution_history.rb +46 -0
- data/lib/dynflow/execution_plan.rb +19 -15
- data/lib/dynflow/executors.rb +0 -1
- data/lib/dynflow/executors/abstract.rb +5 -10
- data/lib/dynflow/executors/parallel.rb +16 -13
- data/lib/dynflow/executors/parallel/core.rb +76 -78
- data/lib/dynflow/executors/parallel/execution_plan_manager.rb +4 -5
- data/lib/dynflow/executors/parallel/pool.rb +22 -52
- data/lib/dynflow/executors/parallel/running_steps_manager.rb +9 -2
- data/lib/dynflow/executors/parallel/worker.rb +5 -10
- data/lib/dynflow/persistence.rb +14 -0
- data/lib/dynflow/persistence_adapters/abstract.rb +14 -3
- data/lib/dynflow/persistence_adapters/sequel.rb +142 -38
- data/lib/dynflow/persistence_adapters/sequel_migrations/004_coordinator_records.rb +14 -0
- data/lib/dynflow/persistence_adapters/sequel_migrations/005_envelopes.rb +14 -0
- data/lib/dynflow/round_robin.rb +37 -0
- data/lib/dynflow/serializable.rb +1 -2
- data/lib/dynflow/serializer.rb +46 -0
- data/lib/dynflow/testing/dummy_executor.rb +2 -2
- data/lib/dynflow/testing/dummy_world.rb +1 -1
- data/lib/dynflow/transaction_adapters/abstract.rb +0 -5
- data/lib/dynflow/transaction_adapters/active_record.rb +0 -10
- data/lib/dynflow/version.rb +1 -1
- data/lib/dynflow/web.rb +26 -0
- data/lib/dynflow/web/console.rb +108 -0
- data/lib/dynflow/web/console_helpers.rb +158 -0
- data/lib/dynflow/web/filtering_helpers.rb +85 -0
- data/lib/dynflow/web/world_helpers.rb +9 -0
- data/lib/dynflow/web_console.rb +3 -310
- data/lib/dynflow/world.rb +188 -119
- data/test/abnormal_states_recovery_test.rb +152 -0
- data/test/action_test.rb +2 -3
- data/test/clock_test.rb +1 -5
- data/test/coordinator_test.rb +152 -0
- data/test/dispatcher_test.rb +146 -0
- data/test/execution_plan_test.rb +2 -1
- data/test/executor_test.rb +534 -612
- data/test/middleware_test.rb +4 -4
- data/test/persistence_test.rb +17 -0
- data/test/prepare_travis_env.sh +35 -0
- data/test/rescue_test.rb +5 -3
- data/test/round_robin_test.rb +28 -0
- data/test/support/code_workflow_example.rb +0 -73
- data/test/support/dummy_example.rb +130 -0
- data/test/support/test_execution_log.rb +41 -0
- data/test/test_helper.rb +222 -116
- data/test/testing_test.rb +10 -10
- data/test/web_console_test.rb +3 -3
- data/test/world_test.rb +23 -0
- data/web/assets/images/logo-square.png +0 -0
- data/web/assets/stylesheets/application.css +9 -0
- data/web/assets/vendor/bootstrap/config.json +429 -0
- data/web/assets/vendor/bootstrap/css/bootstrap-theme.css +479 -0
- data/web/assets/vendor/bootstrap/css/bootstrap-theme.min.css +10 -0
- data/web/assets/vendor/bootstrap/css/bootstrap.css +5377 -4980
- data/web/assets/vendor/bootstrap/css/bootstrap.min.css +9 -8
- data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.eot +0 -0
- data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.svg +288 -0
- data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf +0 -0
- data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff +0 -0
- data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff2 +0 -0
- data/web/assets/vendor/bootstrap/js/bootstrap.js +1674 -1645
- data/web/assets/vendor/bootstrap/js/bootstrap.min.js +11 -5
- data/web/views/execution_history.erb +17 -0
- data/web/views/index.erb +4 -6
- data/web/views/layout.erb +44 -8
- data/web/views/show.erb +4 -5
- data/web/views/worlds.erb +26 -0
- metadata +116 -23
- checksums.yaml +0 -15
- data/lib/dynflow/daemon.rb +0 -30
- data/lib/dynflow/executors/remote_via_socket.rb +0 -43
- data/lib/dynflow/executors/remote_via_socket/core.rb +0 -184
- data/lib/dynflow/future.rb +0 -173
- data/lib/dynflow/listeners.rb +0 -7
- data/lib/dynflow/listeners/abstract.rb +0 -17
- data/lib/dynflow/listeners/serialization.rb +0 -77
- data/lib/dynflow/listeners/socket.rb +0 -117
- data/lib/dynflow/micro_actor.rb +0 -102
- data/lib/dynflow/simple_world.rb +0 -19
- data/test/remote_via_socket_test.rb +0 -170
- data/web/assets/vendor/bootstrap/css/bootstrap-responsive.css +0 -1109
- data/web/assets/vendor/bootstrap/css/bootstrap-responsive.min.css +0 -9
- data/web/assets/vendor/bootstrap/img/glyphicons-halflings-white.png +0 -0
- data/web/assets/vendor/bootstrap/img/glyphicons-halflings.png +0 -0
data/test/middleware_test.rb
CHANGED
@@ -4,7 +4,7 @@ module Dynflow
|
|
4
4
|
module MiddlewareTest
|
5
5
|
|
6
6
|
describe 'Middleware' do
|
7
|
-
let(:world) {
|
7
|
+
let(:world) { WorldFactory.create_world }
|
8
8
|
let(:log) { Support::MiddlewareExample::LogMiddleware.log }
|
9
9
|
|
10
10
|
before do
|
@@ -39,7 +39,7 @@ module Dynflow
|
|
39
39
|
|
40
40
|
describe "world.middleware" do
|
41
41
|
let(:world_with_middleware) do
|
42
|
-
|
42
|
+
WorldFactory.create_world.tap do |world|
|
43
43
|
world.middleware.use(Support::MiddlewareExample::AnotherLogRunMiddleware)
|
44
44
|
end
|
45
45
|
end
|
@@ -68,7 +68,7 @@ module Dynflow
|
|
68
68
|
|
69
69
|
describe "after" do
|
70
70
|
let(:world_with_middleware) do
|
71
|
-
|
71
|
+
WorldFactory.create_world.tap do |world|
|
72
72
|
world.middleware.use(Support::MiddlewareExample::AnotherLogRunMiddleware,
|
73
73
|
after: Support::MiddlewareExample::LogRunMiddleware)
|
74
74
|
|
@@ -96,7 +96,7 @@ module Dynflow
|
|
96
96
|
end
|
97
97
|
|
98
98
|
it "allows access the running action" do
|
99
|
-
world =
|
99
|
+
world = WorldFactory.create_world
|
100
100
|
world.middleware.use(Support::MiddlewareExample::ObservingMiddleware,
|
101
101
|
replace: Support::MiddlewareExample::LogRunMiddleware)
|
102
102
|
world.trigger(Support::MiddlewareExample::Action, message: 'hello').finished.wait
|
data/test/persistence_test.rb
CHANGED
@@ -185,6 +185,23 @@ module Dynflow
|
|
185
185
|
end
|
186
186
|
end
|
187
187
|
end
|
188
|
+
|
189
|
+
it "supports connector's needs for exchaning envelopes" do
|
190
|
+
client_world_id = '5678'
|
191
|
+
executor_world_id = '1234'
|
192
|
+
envelope_hash = ->(envelope) { Dynflow.serializer.dump(envelope).with_indifferent_access }
|
193
|
+
executor_envelope = envelope_hash.call(Dispatcher::Envelope[123, client_world_id, executor_world_id, Dispatcher::Execution['111']])
|
194
|
+
client_envelope = envelope_hash.call(Dispatcher::Envelope[123, executor_world_id, client_world_id, Dispatcher::Accepted])
|
195
|
+
envelopes = [client_envelope, executor_envelope]
|
196
|
+
|
197
|
+
envelopes.each { |e| adapter.push_envelope(e) }
|
198
|
+
|
199
|
+
assert_equal [executor_envelope], adapter.pull_envelopes(executor_world_id)
|
200
|
+
assert_equal [client_envelope], adapter.pull_envelopes(client_world_id)
|
201
|
+
assert_equal [], adapter.pull_envelopes(client_world_id)
|
202
|
+
assert_equal [], adapter.pull_envelopes(executor_world_id)
|
203
|
+
end
|
204
|
+
|
188
205
|
end
|
189
206
|
end
|
190
207
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
#!/usr/bin/env bash
|
2
|
+
|
3
|
+
echo "Setting the environment to use ${DB} database"
|
4
|
+
|
5
|
+
BUNDLE_CONFIG=.bundle/config
|
6
|
+
mkdir -p $(dirname $BUNDLE_CONFIG)
|
7
|
+
cat <<EOF > $BUNDLE_CONFIG
|
8
|
+
---
|
9
|
+
BUNDLE_WITHOUT: pry:mysql:postgresql:concurrent_ruby_ext
|
10
|
+
EOF
|
11
|
+
|
12
|
+
case $DB in
|
13
|
+
mysql)
|
14
|
+
sed -i 's/:mysql//'g $BUNDLE_CONFIG
|
15
|
+
mysql -e 'create database travis_ci_test;'
|
16
|
+
;;
|
17
|
+
postgresql)
|
18
|
+
sed -i 's/:postgresql//'g $BUNDLE_CONFIG
|
19
|
+
psql -c 'create database travis_ci_test;' -U postgres
|
20
|
+
;;
|
21
|
+
sqlite3)
|
22
|
+
# the tests are by default using sqlite3: do nothing
|
23
|
+
;;
|
24
|
+
*)
|
25
|
+
echo "Unsupported database ${DB}"
|
26
|
+
exit 1
|
27
|
+
;;
|
28
|
+
esac
|
29
|
+
|
30
|
+
if [ "$CONCURRENT_RUBY_EXT" = "true" ]; then
|
31
|
+
echo "Enabling concurrent-ruby-ext"
|
32
|
+
sed -i 's/:concurrent_ruby_ext//'g $BUNDLE_CONFIG
|
33
|
+
fi
|
34
|
+
|
35
|
+
bundle install
|
data/test/rescue_test.rb
CHANGED
@@ -6,7 +6,7 @@ module Dynflow
|
|
6
6
|
|
7
7
|
Example = Support::RescueExample
|
8
8
|
|
9
|
-
|
9
|
+
let(:world) { WorldFactory.create_world }
|
10
10
|
|
11
11
|
def execute(*args)
|
12
12
|
plan = world.plan(*args)
|
@@ -115,8 +115,10 @@ module Dynflow
|
|
115
115
|
|
116
116
|
describe 'auto rescue' do
|
117
117
|
|
118
|
-
|
119
|
-
|
118
|
+
let(:world) do
|
119
|
+
WorldFactory.create_world do |config|
|
120
|
+
config.auto_rescue = true
|
121
|
+
end
|
120
122
|
end
|
121
123
|
|
122
124
|
describe 'of plan with skips' do
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require_relative 'test_helper'
|
3
|
+
|
4
|
+
module Dynflow
|
5
|
+
module RoundRobinTest
|
6
|
+
describe RoundRobin do
|
7
|
+
let(:rr) { Dynflow::RoundRobin.new }
|
8
|
+
specify do
|
9
|
+
rr.next.must_be_nil
|
10
|
+
rr.next.must_be_nil
|
11
|
+
rr.must_be_empty
|
12
|
+
rr.add 1
|
13
|
+
rr.next.must_equal 1
|
14
|
+
rr.next.must_equal 1
|
15
|
+
rr.add 2
|
16
|
+
rr.next.must_equal 2
|
17
|
+
rr.next.must_equal 1
|
18
|
+
rr.next.must_equal 2
|
19
|
+
rr.delete 1
|
20
|
+
rr.next.must_equal 2
|
21
|
+
rr.next.must_equal 2
|
22
|
+
rr.delete 2
|
23
|
+
rr.next.must_be_nil
|
24
|
+
rr.must_be_empty
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -34,19 +34,6 @@ module Support
|
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
37
|
-
class Slow < Dynflow::Action
|
38
|
-
def plan(seconds)
|
39
|
-
plan_self interval: seconds
|
40
|
-
end
|
41
|
-
|
42
|
-
def run
|
43
|
-
sleep input[:interval]
|
44
|
-
action_logger.debug 'done with sleeping'
|
45
|
-
$slow_actions_done ||= 0
|
46
|
-
$slow_actions_done +=1
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
37
|
class IncomingIssue < Dynflow::Action
|
51
38
|
|
52
39
|
def plan(issue)
|
@@ -311,65 +298,5 @@ module Support
|
|
311
298
|
end
|
312
299
|
end
|
313
300
|
|
314
|
-
class DummySuspended < Dynflow::Action
|
315
|
-
include Dynflow::Action::Polling
|
316
|
-
|
317
|
-
def invoke_external_task
|
318
|
-
error! 'Trolling detected' if input[:text] == 'troll setup'
|
319
|
-
{ progress: 0, done: false }
|
320
|
-
end
|
321
|
-
|
322
|
-
def poll_external_task
|
323
|
-
if input[:text] == 'troll progress' && !output[:trolled]
|
324
|
-
output[:trolled] = true
|
325
|
-
error! 'Trolling detected'
|
326
|
-
end
|
327
|
-
|
328
|
-
if input[:text] =~ /pause in progress (\d+)/
|
329
|
-
TestPause.pause if external_task[:progress] == $1.to_i
|
330
|
-
end
|
331
|
-
|
332
|
-
progress = external_task[:progress] + 10
|
333
|
-
{ progress: progress, done: progress >= 100 }
|
334
|
-
end
|
335
|
-
|
336
|
-
def done?
|
337
|
-
external_task && external_task[:progress] >= 100
|
338
|
-
end
|
339
|
-
|
340
|
-
def poll_interval
|
341
|
-
0.001
|
342
|
-
end
|
343
|
-
|
344
|
-
def run_progress
|
345
|
-
external_task && external_task[:progress].to_f / 100
|
346
|
-
end
|
347
|
-
end
|
348
|
-
|
349
|
-
class DummyHeavyProgress < Dynflow::Action
|
350
|
-
|
351
|
-
def plan(input)
|
352
|
-
sequence do
|
353
|
-
plan_self(input)
|
354
|
-
plan_action(DummySuspended, input)
|
355
|
-
end
|
356
|
-
end
|
357
|
-
|
358
|
-
def run
|
359
|
-
end
|
360
|
-
|
361
|
-
def finalize
|
362
|
-
$dummy_heavy_progress = 'dummy_heavy_progress'
|
363
|
-
end
|
364
|
-
|
365
|
-
def run_progress_weight
|
366
|
-
4
|
367
|
-
end
|
368
|
-
|
369
|
-
def finalize_progress_weight
|
370
|
-
5
|
371
|
-
end
|
372
|
-
end
|
373
|
-
|
374
301
|
end
|
375
302
|
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Support
|
4
|
+
module DummyExample
|
5
|
+
class Dummy < Dynflow::Action
|
6
|
+
def run; end
|
7
|
+
end
|
8
|
+
|
9
|
+
class FailingDummy < Dynflow::Action
|
10
|
+
def run; raise 'error'; end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Slow < Dynflow::Action
|
14
|
+
def plan(seconds)
|
15
|
+
sequence do
|
16
|
+
plan_self interval: seconds
|
17
|
+
plan_action Dummy
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def run
|
22
|
+
sleep input[:interval]
|
23
|
+
action_logger.debug 'done with sleeping'
|
24
|
+
$slow_actions_done ||= 0
|
25
|
+
$slow_actions_done +=1
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Polling < Dynflow::Action
|
30
|
+
include Dynflow::Action::Polling
|
31
|
+
|
32
|
+
def invoke_external_task
|
33
|
+
error! 'Trolling detected' if input[:text] == 'troll setup'
|
34
|
+
{ progress: 0, done: false }
|
35
|
+
end
|
36
|
+
|
37
|
+
def poll_external_task
|
38
|
+
if input[:text] == 'troll progress' && !output[:trolled]
|
39
|
+
output[:trolled] = true
|
40
|
+
error! 'Trolling detected'
|
41
|
+
end
|
42
|
+
|
43
|
+
if input[:text] =~ /pause in progress (\d+)/
|
44
|
+
TestPause.pause if external_task[:progress] == $1.to_i
|
45
|
+
end
|
46
|
+
|
47
|
+
progress = external_task[:progress] + 10
|
48
|
+
{ progress: progress, done: progress >= 100 }
|
49
|
+
end
|
50
|
+
|
51
|
+
def done?
|
52
|
+
external_task && external_task[:progress] >= 100
|
53
|
+
end
|
54
|
+
|
55
|
+
def poll_interval
|
56
|
+
0.001
|
57
|
+
end
|
58
|
+
|
59
|
+
def run_progress
|
60
|
+
external_task && external_task[:progress].to_f / 100
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class WeightedPolling < Dynflow::Action
|
65
|
+
|
66
|
+
def plan(input)
|
67
|
+
sequence do
|
68
|
+
plan_self(input)
|
69
|
+
plan_action(Polling, input)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def run
|
74
|
+
end
|
75
|
+
|
76
|
+
def finalize
|
77
|
+
$dummy_heavy_progress = 'dummy_heavy_progress'
|
78
|
+
end
|
79
|
+
|
80
|
+
def run_progress_weight
|
81
|
+
4
|
82
|
+
end
|
83
|
+
|
84
|
+
def finalize_progress_weight
|
85
|
+
5
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class EventedAction < Dynflow::Action
|
90
|
+
def run(event = nil)
|
91
|
+
case event
|
92
|
+
when "timeout"
|
93
|
+
output[:event] = 'timeout'
|
94
|
+
raise "action timeouted"
|
95
|
+
when nil
|
96
|
+
suspend do |suspended_action|
|
97
|
+
if input[:timeout]
|
98
|
+
world.clock.ping suspended_action, input[:timeout], "timeout"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
else
|
102
|
+
self.output[:event] = event
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class ComposedAction < Dynflow::Action
|
108
|
+
def run(event = nil)
|
109
|
+
match event,
|
110
|
+
(on nil do
|
111
|
+
sub_plan = world.trigger(Dummy)
|
112
|
+
output[:sub_plan_id] = sub_plan.id
|
113
|
+
suspend do |suspended_action|
|
114
|
+
if input[:timeout]
|
115
|
+
world.clock.ping suspended_action, input[:timeout], "timeout"
|
116
|
+
end
|
117
|
+
|
118
|
+
sub_plan.finished.on_success! { suspended_action << 'finish' }
|
119
|
+
end
|
120
|
+
end),
|
121
|
+
(on 'finish' do
|
122
|
+
output[:event] = 'finish'
|
123
|
+
end),
|
124
|
+
(on 'timeout' do
|
125
|
+
output[:event] = 'timeout'
|
126
|
+
end)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
class TestExecutionLog
|
2
|
+
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@log = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def <<(action)
|
10
|
+
@log << [action.class, action.input]
|
11
|
+
end
|
12
|
+
|
13
|
+
def log
|
14
|
+
@log
|
15
|
+
end
|
16
|
+
|
17
|
+
def each(&block)
|
18
|
+
@log.each(&block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def size
|
22
|
+
@log.size
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.setup
|
26
|
+
@run, @finalize = self.new, self.new
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.teardown
|
30
|
+
@run, @finalize = nil, nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.run
|
34
|
+
@run || []
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.finalize
|
38
|
+
@finalize || []
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -12,60 +12,22 @@ $LOAD_PATH << load_path unless $LOAD_PATH.include? load_path
|
|
12
12
|
|
13
13
|
require 'dynflow'
|
14
14
|
require 'dynflow/testing'
|
15
|
-
require 'pry'
|
15
|
+
begin require 'pry'; rescue LoadError; nil end
|
16
16
|
|
17
17
|
require 'support/code_workflow_example'
|
18
18
|
require 'support/middleware_example'
|
19
19
|
require 'support/rescue_example'
|
20
|
+
require 'support/dummy_example'
|
21
|
+
require 'support/test_execution_log'
|
20
22
|
|
21
|
-
|
22
|
-
|
23
|
-
include Enumerable
|
24
|
-
|
25
|
-
def initialize
|
26
|
-
@log = []
|
27
|
-
end
|
28
|
-
|
29
|
-
def <<(action)
|
30
|
-
@log << [action.class, action.input]
|
31
|
-
end
|
32
|
-
|
33
|
-
def log
|
34
|
-
@log
|
35
|
-
end
|
36
|
-
|
37
|
-
def each(&block)
|
38
|
-
@log.each(&block)
|
39
|
-
end
|
40
|
-
|
41
|
-
def size
|
42
|
-
@log.size
|
43
|
-
end
|
44
|
-
|
45
|
-
def self.setup
|
46
|
-
@run, @finalize = self.new, self.new
|
47
|
-
end
|
48
|
-
|
49
|
-
def self.teardown
|
50
|
-
@run, @finalize = nil, nil
|
51
|
-
end
|
52
|
-
|
53
|
-
def self.run
|
54
|
-
@run || []
|
55
|
-
end
|
56
|
-
|
57
|
-
def self.finalize
|
58
|
-
@finalize || []
|
59
|
-
end
|
60
|
-
|
61
|
-
end
|
23
|
+
Concurrent.disable_executor_auto_termination!
|
62
24
|
|
63
25
|
# To be able to stop a process in some step and perform assertions while paused
|
64
26
|
class TestPause
|
65
27
|
|
66
28
|
def self.setup
|
67
|
-
@pause =
|
68
|
-
@ready =
|
29
|
+
@pause = Concurrent.future
|
30
|
+
@ready = Concurrent.future
|
69
31
|
end
|
70
32
|
|
71
33
|
def self.teardown
|
@@ -77,10 +39,10 @@ class TestPause
|
|
77
39
|
def self.pause
|
78
40
|
if !@pause
|
79
41
|
raise 'the TestPause class was not setup'
|
80
|
-
elsif @ready.
|
42
|
+
elsif @ready.completed?
|
81
43
|
raise 'you can pause only once'
|
82
44
|
else
|
83
|
-
@ready.
|
45
|
+
@ready.success(true)
|
84
46
|
@pause.wait
|
85
47
|
end
|
86
48
|
end
|
@@ -90,125 +52,269 @@ class TestPause
|
|
90
52
|
if @pause
|
91
53
|
@ready.wait # wait till we are paused
|
92
54
|
yield
|
93
|
-
@pause.
|
55
|
+
@pause.success(true) # resume the run
|
94
56
|
else
|
95
57
|
raise 'the TestPause class was not setup'
|
96
58
|
end
|
97
59
|
end
|
98
60
|
end
|
99
61
|
|
100
|
-
|
101
|
-
|
102
|
-
|
62
|
+
class CoordiationAdapterWithLog < Dynflow::CoordinatorAdapters::Sequel
|
63
|
+
attr_reader :lock_log
|
64
|
+
def initialize(*args)
|
65
|
+
@lock_log = []
|
66
|
+
super
|
67
|
+
end
|
68
|
+
|
69
|
+
def create_record(record)
|
70
|
+
@lock_log << "lock #{record.id}" if record.is_a? Dynflow::Coordinator::Lock
|
71
|
+
super
|
103
72
|
end
|
104
73
|
|
105
|
-
def
|
106
|
-
|
107
|
-
|
108
|
-
@remote_world
|
74
|
+
def delete_record(record)
|
75
|
+
@lock_log << "unlock #{record.id}" if record.is_a? Dynflow::Coordinator::Lock
|
76
|
+
super
|
109
77
|
end
|
78
|
+
end
|
79
|
+
|
80
|
+
module WorldFactory
|
110
81
|
|
82
|
+
def self.created_worlds
|
83
|
+
@created_worlds ||= []
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.test_world_config
|
87
|
+
config = Dynflow::Config.new
|
88
|
+
config.persistence_adapter = persistence_adapter
|
89
|
+
config.logger_adapter = logger_adapter
|
90
|
+
config.coordinator_adapter = coordinator_adapter
|
91
|
+
config.auto_rescue = false
|
92
|
+
config.exit_on_terminate = false
|
93
|
+
config.auto_execute = false
|
94
|
+
config.auto_terminate = false
|
95
|
+
yield config if block_given?
|
96
|
+
return config
|
97
|
+
end
|
98
|
+
|
99
|
+
# The worlds created by this method are getting terminated after each test run
|
100
|
+
def self.create_world(&block)
|
101
|
+
Dynflow::World.new(test_world_config(&block)).tap do |world|
|
102
|
+
created_worlds << world
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# This world survives though the whole run of the test suite: careful with it, it can
|
107
|
+
# introduce unnecessary test dependencies
|
111
108
|
def self.logger_adapter
|
112
109
|
@adapter ||= Dynflow::LoggerAdapters::Simple.new $stderr, 4
|
113
110
|
end
|
114
111
|
|
115
|
-
def self.
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
auto_rescue: false }.merge(options)
|
122
|
-
Dynflow::World.new(options)
|
112
|
+
def self.persistence_adapter
|
113
|
+
@persistence_adapter ||= begin
|
114
|
+
db_config = ENV['DB_CONN_STRING'] || 'sqlite:/'
|
115
|
+
puts "Using database configuration: #{db_config}"
|
116
|
+
Dynflow::PersistenceAdapters::Sequel.new(db_config)
|
117
|
+
end
|
123
118
|
end
|
124
119
|
|
125
|
-
def self.
|
126
|
-
|
127
|
-
socket_path = Dir.tmpdir + "/dynflow_remote_#{@counter+=1}"
|
128
|
-
listener = Dynflow::Listeners::Socket.new world, socket_path
|
129
|
-
world = Dynflow::World.new(
|
130
|
-
logger_adapter: logger_adapter,
|
131
|
-
auto_terminate: false,
|
132
|
-
exit_on_terminate: false,
|
133
|
-
persistence_adapter: -> remote_world { world.persistence.adapter },
|
134
|
-
transaction_adapter: Dynflow::TransactionAdapters::None.new,
|
135
|
-
executor: -> remote_world do
|
136
|
-
Dynflow::Executors::RemoteViaSocket.new(remote_world, socket_path)
|
137
|
-
end)
|
138
|
-
return listener, world
|
120
|
+
def self.coordinator_adapter
|
121
|
+
->(world, _) { CoordiationAdapterWithLog.new(world) }
|
139
122
|
end
|
140
123
|
|
141
|
-
def self.
|
142
|
-
|
143
|
-
|
124
|
+
def self.clean_coordinator_records
|
125
|
+
persistence_adapter = WorldFactory.persistence_adapter
|
126
|
+
persistence_adapter.find_coordinator_records({}).each do |w|
|
127
|
+
warn "Unexpected coordinator record: #{ w }"
|
128
|
+
persistence_adapter.delete_coordinator_record(w[:class], w[:id])
|
129
|
+
end
|
130
|
+
end
|
144
131
|
|
145
|
-
|
132
|
+
def self.terminate_worlds
|
133
|
+
created_worlds.map(&:terminate).map(&:wait)
|
134
|
+
created_worlds.clear
|
146
135
|
end
|
136
|
+
end
|
147
137
|
|
148
|
-
|
149
|
-
|
138
|
+
module TestHelpers
|
139
|
+
# allows to create the world inside the tests, using the `connector`
|
140
|
+
# and `persistence adapter` from the test context: usefull to create
|
141
|
+
# multi-world topology for a signle test
|
142
|
+
def create_world(with_executor = true)
|
143
|
+
WorldFactory.create_world do |config|
|
144
|
+
config.connector = connector
|
145
|
+
config.persistence_adapter = persistence_adapter
|
146
|
+
unless with_executor
|
147
|
+
config.executor = false
|
148
|
+
end
|
149
|
+
end
|
150
150
|
end
|
151
151
|
|
152
|
-
def
|
153
|
-
|
152
|
+
def connector_polling_interval(world)
|
153
|
+
if world.persistence.adapter.db.class.name == "Sequel::Postgres::Database"
|
154
|
+
5
|
155
|
+
else
|
156
|
+
0.005
|
157
|
+
end
|
154
158
|
end
|
155
|
-
end
|
156
159
|
|
157
|
-
#
|
158
|
-
|
159
|
-
|
160
|
-
|
160
|
+
# waits for the passed block to return non-nil value and reiterates it while getting false
|
161
|
+
# (till some reasonable timeout). Useful for forcing the tests for some event to occur
|
162
|
+
def wait_for
|
163
|
+
30.times do
|
164
|
+
ret = yield
|
165
|
+
return ret if ret
|
166
|
+
sleep 0.3
|
167
|
+
end
|
168
|
+
raise 'waiting for something to happend was not successful'
|
169
|
+
end
|
161
170
|
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
171
|
+
def executor_id_for_plan(execution_plan_id)
|
172
|
+
if lock = client_world.coordinator.find_locks(class: Dynflow::Coordinator::ExecutionLock.name,
|
173
|
+
id: "execution-plan:#{execution_plan_id}").first
|
174
|
+
lock.world_id
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def trigger_waiting_action
|
179
|
+
triggered = client_world.trigger(Support::DummyExample::EventedAction)
|
180
|
+
wait_for { executor_id_for_plan(triggered.id) } # waiting for the plan to be picked by an executor
|
181
|
+
triggered
|
182
|
+
end
|
183
|
+
|
184
|
+
# trigger an action, and keep it running while yielding the block
|
185
|
+
def while_executing_plan
|
186
|
+
triggered = trigger_waiting_action
|
187
|
+
|
188
|
+
executor_id = wait_for do
|
189
|
+
executor_id_for_plan(triggered.id)
|
190
|
+
end
|
191
|
+
|
192
|
+
wait_for do
|
193
|
+
client_world.persistence.load_execution_plan(triggered.id).state == :running
|
168
194
|
end
|
195
|
+
|
196
|
+
executor = WorldFactory.created_worlds.find { |e| e.id == executor_id }
|
197
|
+
raise "Could not find an executor with id #{executor_id}" unless executor
|
198
|
+
yield executor
|
199
|
+
return triggered
|
200
|
+
end
|
201
|
+
|
202
|
+
# finish the plan triggered by the `while_executing_plan` method
|
203
|
+
def finish_the_plan(triggered)
|
204
|
+
wait_for do
|
205
|
+
client_world.persistence.load_execution_plan(triggered.id).state == :running &&
|
206
|
+
client_world.persistence.load_step(triggered.id, 2, client_world).state == :suspended
|
207
|
+
end
|
208
|
+
client_world.event(triggered.id, 2, 'finish')
|
209
|
+
return triggered.finished.value
|
210
|
+
end
|
211
|
+
|
212
|
+
def assert_plan_reexecuted(plan)
|
213
|
+
assert_equal :stopped, plan.state
|
214
|
+
assert_equal :success, plan.result
|
215
|
+
assert_equal plan.execution_history.map(&:name),
|
216
|
+
['start execution',
|
217
|
+
'terminate execution',
|
218
|
+
'start execution',
|
219
|
+
'finish execution']
|
220
|
+
refute_equal plan.execution_history.first.world_id, plan.execution_history.to_a.last.world_id
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
class MiniTest::Test
|
225
|
+
def setup
|
226
|
+
WorldFactory.clean_coordinator_records
|
227
|
+
end
|
228
|
+
|
229
|
+
def teardown
|
230
|
+
WorldFactory.terminate_worlds
|
169
231
|
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# ensure there are no unresolved events at the end or being GCed
|
235
|
+
events_test = -> do
|
236
|
+
event_creations = {}
|
237
|
+
non_ready_events = {}
|
170
238
|
|
171
|
-
|
172
|
-
super(*args, &block).tap do |
|
173
|
-
|
174
|
-
non_ready_futures[f.object_id] = f
|
239
|
+
Concurrent::Edge::Event.singleton_class.send :define_method, :new do |*args, &block|
|
240
|
+
super(*args, &block).tap do |event|
|
241
|
+
event_creations[event.object_id] = caller(4)
|
175
242
|
end
|
176
243
|
end
|
177
244
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
245
|
+
[Concurrent::Edge::Event, Concurrent::Edge::Future].each do |future_class|
|
246
|
+
original_complete_method = future_class.instance_method :complete_with
|
247
|
+
future_class.send :define_method, :complete_with do |*args|
|
248
|
+
begin
|
249
|
+
original_complete_method.bind(self).call(*args)
|
250
|
+
ensure
|
251
|
+
event_creations.delete(self.object_id)
|
252
|
+
end
|
184
253
|
end
|
185
254
|
end
|
186
255
|
|
187
256
|
MiniTest.after_run do
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
257
|
+
Concurrent::Actor.root.ask!(:terminate!)
|
258
|
+
|
259
|
+
non_ready_events = ObjectSpace.each_object(Concurrent::Edge::Event).map do |event|
|
260
|
+
event.wait(1)
|
261
|
+
unless event.completed?
|
262
|
+
event.object_id
|
193
263
|
end
|
194
|
-
|
195
|
-
|
196
|
-
|
264
|
+
end.compact
|
265
|
+
|
266
|
+
# make sure to include the ids that were garbage-collected already
|
267
|
+
non_ready_events = (non_ready_events + event_creations.keys).uniq
|
268
|
+
|
269
|
+
unless non_ready_events.empty?
|
270
|
+
unified = non_ready_events.each_with_object({}) do |(id, _), h|
|
271
|
+
backtrace_key = event_creations[id].hash
|
272
|
+
h[backtrace_key] ||= []
|
273
|
+
h[backtrace_key] << id
|
274
|
+
end
|
275
|
+
raise("there were #{non_ready_events.size} non_ready_events:\n" +
|
276
|
+
unified.map do |_, ids|
|
277
|
+
"--- #{ids.size}: #{ids}\n#{event_creations[ids.first].join("\n")}"
|
197
278
|
end.join("\n"))
|
198
279
|
end
|
199
280
|
end
|
200
281
|
|
201
282
|
# time out all futures by default
|
202
283
|
default_timeout = 8
|
203
|
-
wait_method =
|
284
|
+
wait_method = Concurrent::Edge::Event.instance_method(:wait)
|
204
285
|
|
205
|
-
|
286
|
+
Concurrent::Edge::Event.class_eval do
|
206
287
|
define_method :wait do |timeout = nil|
|
207
288
|
wait_method.bind(self).call(timeout || default_timeout)
|
208
289
|
end
|
209
290
|
end
|
210
291
|
|
211
|
-
end
|
292
|
+
end
|
293
|
+
|
294
|
+
events_test.call
|
295
|
+
|
296
|
+
class ConcurrentRunTester
|
297
|
+
def initialize
|
298
|
+
@enter_future, @exit_future = Concurrent.future, Concurrent.future
|
299
|
+
end
|
300
|
+
|
301
|
+
def while_executing(&block)
|
302
|
+
@thread = Thread.new do
|
303
|
+
block.call(self)
|
304
|
+
end
|
305
|
+
@enter_future.wait(1)
|
306
|
+
end
|
307
|
+
|
308
|
+
def pause
|
309
|
+
@enter_future.success(true)
|
310
|
+
@exit_future.wait(1)
|
311
|
+
end
|
312
|
+
|
313
|
+
def finish
|
314
|
+
@exit_future.success(true)
|
315
|
+
@thread.join
|
316
|
+
end
|
317
|
+
end
|
212
318
|
|
213
319
|
module PlanAssertions
|
214
320
|
|