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.
- 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
|
|