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
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
require_relative 'test_helper'
|
|
3
|
+
|
|
4
|
+
module Dynflow
|
|
5
|
+
module ConsistencyCheckTest
|
|
6
|
+
|
|
7
|
+
describe "consistency check" do
|
|
8
|
+
|
|
9
|
+
include TestHelpers
|
|
10
|
+
|
|
11
|
+
def with_invalidation_while_executing(finish)
|
|
12
|
+
triggered = while_executing_plan do |executor|
|
|
13
|
+
if Connectors::Direct === executor.connector
|
|
14
|
+
# for better simulation of invalidation with direct executor
|
|
15
|
+
executor.connector.stop_listening(executor)
|
|
16
|
+
end
|
|
17
|
+
client_world.invalidate(executor.registered_world)
|
|
18
|
+
end
|
|
19
|
+
plan = if finish
|
|
20
|
+
finish_the_plan(triggered)
|
|
21
|
+
else
|
|
22
|
+
triggered.finished.wait
|
|
23
|
+
client_world.persistence.load_execution_plan(triggered.id)
|
|
24
|
+
end
|
|
25
|
+
yield plan
|
|
26
|
+
ensure
|
|
27
|
+
# just to workaround state transition checks due to our simulation
|
|
28
|
+
# of second world being inactive
|
|
29
|
+
if plan
|
|
30
|
+
plan.set_state(:running, true)
|
|
31
|
+
plan.save
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
let(:persistence_adapter) { WorldFactory.persistence_adapter }
|
|
36
|
+
let(:shared_connector) { Connectors::Direct.new }
|
|
37
|
+
let(:connector) { Proc.new { |world| shared_connector.start_listening(world); shared_connector } }
|
|
38
|
+
let(:executor_world) { create_world(true) }
|
|
39
|
+
let(:executor_world_2) { create_world(true) }
|
|
40
|
+
let(:client_world) { create_world(false) }
|
|
41
|
+
let(:client_world_2) { create_world(false) }
|
|
42
|
+
|
|
43
|
+
describe "for plans assigned to invalid world" do
|
|
44
|
+
|
|
45
|
+
before do
|
|
46
|
+
# mention the executors to make sure they are initialized
|
|
47
|
+
[executor_world, executor_world_2]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe 'world invalidation' do
|
|
51
|
+
it 'removes the world from the register' do
|
|
52
|
+
client_world.invalidate(executor_world.registered_world)
|
|
53
|
+
worlds = client_world.coordinator.find_worlds
|
|
54
|
+
refute_includes(worlds, executor_world.registered_world)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'schedules the plans to be run on different executor' do
|
|
58
|
+
with_invalidation_while_executing(true) do |plan|
|
|
59
|
+
assert_plan_reexecuted(plan)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'when no executor is available, marks the plans as paused' do
|
|
64
|
+
executor_world_2.terminate.wait
|
|
65
|
+
with_invalidation_while_executing(false) do |plan|
|
|
66
|
+
plan.state.must_equal :paused
|
|
67
|
+
plan.result.must_equal :pending
|
|
68
|
+
expected_history = [['start execution', executor_world.id],
|
|
69
|
+
['terminate execution', executor_world.id]]
|
|
70
|
+
plan.execution_history.map { |h| [h.name, h.world_id] }.must_equal(expected_history)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "prevents from running the invalidation twice on the same world" do
|
|
75
|
+
client_world.invalidate(executor_world.registered_world)
|
|
76
|
+
expected_locks = ["lock world-invalidation:#{executor_world.id}",
|
|
77
|
+
"unlock world-invalidation:#{executor_world.id}"]
|
|
78
|
+
client_world.coordinator.adapter.lock_log.must_equal(expected_locks)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "prevents from running the consistency checks twice on the same world concurrently" do
|
|
82
|
+
client_world.invalidate(executor_world.registered_world)
|
|
83
|
+
expected_locks = ["lock world-invalidation:#{executor_world.id}",
|
|
84
|
+
"unlock world-invalidation:#{executor_world.id}"]
|
|
85
|
+
client_world.coordinator.adapter.lock_log.must_equal(expected_locks)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
describe 'auto execute' do
|
|
91
|
+
|
|
92
|
+
before do
|
|
93
|
+
client_world.persistence.find_execution_plans({}).each do |plan|
|
|
94
|
+
# make sure we don't handle plans from previous tests
|
|
95
|
+
# TODO: delete the plans instead, once we have
|
|
96
|
+
# https://github.com/Dynflow/dynflow/pull/141 merged
|
|
97
|
+
plan.set_state(:stopped, true)
|
|
98
|
+
plan.save
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it "prevents from running the auto-execution twice" do
|
|
103
|
+
client_world.auto_execute
|
|
104
|
+
expected_locks = ["lock auto-execute", "unlock auto-execute"]
|
|
105
|
+
client_world.coordinator.adapter.lock_log.must_equal(expected_locks)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "re-runs the plans that were planned but not executed" do
|
|
109
|
+
triggered = client_world.trigger(Support::DummyExample::Dummy)
|
|
110
|
+
triggered.finished.wait
|
|
111
|
+
executor_world.auto_execute
|
|
112
|
+
plan = wait_for do
|
|
113
|
+
plan = client_world.persistence.load_execution_plan(triggered.id)
|
|
114
|
+
if plan.state == :stopped
|
|
115
|
+
plan
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
expected_history = [['start execution', executor_world.id],
|
|
119
|
+
['finish execution', executor_world.id]]
|
|
120
|
+
plan.execution_history.map { |h| [h.name, h.world_id] }.must_equal(expected_history)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "re-runs the plans that were terminated but not re-executed (because no available executor)" do
|
|
124
|
+
executor_world # mention it to get initialized
|
|
125
|
+
triggered = while_executing_plan { |executor| executor.terminate.wait }
|
|
126
|
+
executor_world_2.auto_execute
|
|
127
|
+
finish_the_plan(triggered)
|
|
128
|
+
plan = wait_for do
|
|
129
|
+
plan = client_world.persistence.load_execution_plan(triggered.id)
|
|
130
|
+
if plan.state == :stopped
|
|
131
|
+
plan
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
assert_plan_reexecuted(plan)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it "doesn't rerun the plans that were paused with error" do
|
|
138
|
+
executor_world # mention it to get initialized
|
|
139
|
+
triggered = client_world.trigger(Support::DummyExample::FailingDummy)
|
|
140
|
+
triggered.finished.wait
|
|
141
|
+
executor_world.auto_execute
|
|
142
|
+
plan = client_world.persistence.load_execution_plan(triggered.id)
|
|
143
|
+
plan.state.must_equal :paused
|
|
144
|
+
expected_history = [['start execution', executor_world.id],
|
|
145
|
+
['finish execution', executor_world.id]]
|
|
146
|
+
plan.execution_history.map { |h| [h.name, h.world_id] }.must_equal(expected_history)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
data/test/action_test.rb
CHANGED
|
@@ -2,7 +2,8 @@ require_relative 'test_helper'
|
|
|
2
2
|
|
|
3
3
|
module Dynflow
|
|
4
4
|
describe 'action' do
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
let(:world) { WorldFactory.create_world }
|
|
6
7
|
|
|
7
8
|
describe Action::Missing do
|
|
8
9
|
|
|
@@ -47,7 +48,6 @@ module Dynflow
|
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
describe Action::Present do
|
|
50
|
-
include WorldInstance
|
|
51
51
|
|
|
52
52
|
let :execution_plan do
|
|
53
53
|
result = world.trigger(Support::CodeWorkflowExample::IncomingIssues, issues_data)
|
|
@@ -92,7 +92,6 @@ module Dynflow
|
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
describe '#humanized_state' do
|
|
95
|
-
include WorldInstance
|
|
96
95
|
include Testing
|
|
97
96
|
|
|
98
97
|
class ActionWithHumanizedState < Dynflow::Action
|
data/test/clock_test.rb
CHANGED
|
@@ -5,10 +5,9 @@ clock_class = Dynflow::Clock
|
|
|
5
5
|
|
|
6
6
|
describe clock_class do
|
|
7
7
|
|
|
8
|
-
let(:clock) { clock_class.
|
|
8
|
+
let(:clock) { clock_class.spawn 'clock' }
|
|
9
9
|
|
|
10
10
|
it 'refuses who without #<< method' do
|
|
11
|
-
clock.initialized.wait
|
|
12
11
|
-> { clock.ping Object.new, 0.1, :pong }.must_raise TypeError
|
|
13
12
|
clock.ping [], 0.1, :pong
|
|
14
13
|
end
|
|
@@ -16,7 +15,6 @@ describe clock_class do
|
|
|
16
15
|
|
|
17
16
|
it 'pongs' do
|
|
18
17
|
q = Queue.new
|
|
19
|
-
clock.initialized.wait
|
|
20
18
|
start = Time.now
|
|
21
19
|
|
|
22
20
|
clock.ping q, 0.1, o = Object.new
|
|
@@ -27,7 +25,6 @@ describe clock_class do
|
|
|
27
25
|
|
|
28
26
|
it 'pongs on expected times' do
|
|
29
27
|
q = Queue.new
|
|
30
|
-
clock.initialized.wait
|
|
31
28
|
start = Time.now
|
|
32
29
|
|
|
33
30
|
clock.ping q, 0.3, :a
|
|
@@ -43,7 +40,6 @@ describe clock_class do
|
|
|
43
40
|
end
|
|
44
41
|
|
|
45
42
|
it 'works under stress' do
|
|
46
|
-
clock.initialized.wait
|
|
47
43
|
threads = Array.new(4) do
|
|
48
44
|
Thread.new do
|
|
49
45
|
q = Queue.new
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
require_relative 'test_helper'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
|
|
4
|
+
module Dynflow
|
|
5
|
+
module CoordinatorTest
|
|
6
|
+
describe Coordinator do
|
|
7
|
+
let(:world) { WorldFactory.create_world }
|
|
8
|
+
let(:another_world) { WorldFactory.create_world }
|
|
9
|
+
|
|
10
|
+
describe 'locks' do
|
|
11
|
+
it 'unlocks the lock, when the block is passed' do
|
|
12
|
+
world.coordinator.acquire(Coordinator::AutoExecuteLock.new(world)) {}
|
|
13
|
+
expected_locks = ["lock auto-execute", "unlock auto-execute"]
|
|
14
|
+
world.coordinator.adapter.lock_log.must_equal(expected_locks)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "doesn't unlock, when the block is not passed" do
|
|
18
|
+
world.coordinator.acquire(Coordinator::AutoExecuteLock.new(world))
|
|
19
|
+
expected_locks = ["lock auto-execute"]
|
|
20
|
+
world.coordinator.adapter.lock_log.must_equal(expected_locks)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'supports unlocking by owner' do
|
|
24
|
+
lock = Coordinator::AutoExecuteLock.new(world)
|
|
25
|
+
tester = ConcurrentRunTester.new
|
|
26
|
+
tester.while_executing do
|
|
27
|
+
world.coordinator.acquire(lock)
|
|
28
|
+
tester.pause
|
|
29
|
+
end
|
|
30
|
+
world.coordinator.release_by_owner("world:#{world.id}")
|
|
31
|
+
world.coordinator.acquire(lock) # expected no error raised
|
|
32
|
+
tester.finish
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'deserializes the data from the adapter when searching for locks' do
|
|
36
|
+
lock = Coordinator::AutoExecuteLock.new(world)
|
|
37
|
+
world.coordinator.acquire(lock)
|
|
38
|
+
found_locks = world.coordinator.find_locks(owner_id: lock.owner_id)
|
|
39
|
+
found_locks.size.must_equal 1
|
|
40
|
+
found_locks.first.data.must_equal lock.data
|
|
41
|
+
|
|
42
|
+
found_locks = world.coordinator.find_locks(class: lock.class.name, id: lock.id)
|
|
43
|
+
found_locks.size.must_equal 1
|
|
44
|
+
found_locks.first.data.must_equal lock.data
|
|
45
|
+
|
|
46
|
+
another_lock = Coordinator::AutoExecuteLock.new(another_world)
|
|
47
|
+
found_locks = world.coordinator.find_locks(owner_id: another_lock.owner_id)
|
|
48
|
+
found_locks.size.must_equal 0
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
describe 'records' do
|
|
53
|
+
class DummyRecord < Coordinator::Record
|
|
54
|
+
def initialize(id, value)
|
|
55
|
+
super
|
|
56
|
+
@data[:id] = value
|
|
57
|
+
@data[:value] = value
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def value
|
|
61
|
+
@data[:value]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def value=(value)
|
|
65
|
+
@data[:value] = (value)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'allows CRUD record objects' do
|
|
70
|
+
dummy_record = DummyRecord.new('dummy', 'Foo')
|
|
71
|
+
world.coordinator.create_record(dummy_record)
|
|
72
|
+
saved_dummy_record = world.coordinator.find_records(class: dummy_record.class.name).first
|
|
73
|
+
saved_dummy_record.must_equal dummy_record
|
|
74
|
+
|
|
75
|
+
dummy_record.value = 'Bar'
|
|
76
|
+
world.coordinator.update_record(dummy_record)
|
|
77
|
+
saved_dummy_record = world.coordinator.find_records(class: dummy_record.class.name).first
|
|
78
|
+
saved_dummy_record.data.must_equal dummy_record.data
|
|
79
|
+
|
|
80
|
+
world.coordinator.delete_record(dummy_record)
|
|
81
|
+
world.coordinator.find_records(class: dummy_record.class.name).must_equal []
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
describe 'on termination' do
|
|
86
|
+
it 'removes all the locks assigned to the given world' do
|
|
87
|
+
world.coordinator.acquire(Coordinator::AutoExecuteLock.new(world))
|
|
88
|
+
another_world.coordinator.acquire Coordinator::WorldInvalidationLock.new(another_world, another_world)
|
|
89
|
+
world.terminate.wait
|
|
90
|
+
expected_locks = ["lock auto-execute", "unlock auto-execute"]
|
|
91
|
+
world.coordinator.adapter.lock_log.must_equal(expected_locks)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'prevents new locks to be acquired by the world being terminated' do
|
|
95
|
+
world.terminate
|
|
96
|
+
-> do
|
|
97
|
+
world.coordinator.acquire(Coordinator::AutoExecuteLock.new(world))
|
|
98
|
+
end.must_raise(Errors::InactiveWorldError)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def self.it_supports_global_records
|
|
103
|
+
describe 'records handling' do
|
|
104
|
+
it 'prevents saving the same record twice' do
|
|
105
|
+
record = Coordinator::AutoExecuteLock.new(world)
|
|
106
|
+
tester = ConcurrentRunTester.new
|
|
107
|
+
tester.while_executing do
|
|
108
|
+
adapter.create_record(record)
|
|
109
|
+
tester.pause
|
|
110
|
+
end
|
|
111
|
+
-> { another_adapter.create_record(record) }.must_raise(Coordinator::DuplicateRecordError)
|
|
112
|
+
tester.finish
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'allows saving different records' do
|
|
116
|
+
record = Coordinator::AutoExecuteLock.new(world)
|
|
117
|
+
another_record = Coordinator::WorldInvalidationLock.new(world, another_world)
|
|
118
|
+
tester = ConcurrentRunTester.new
|
|
119
|
+
tester.while_executing do
|
|
120
|
+
adapter.create_record(record)
|
|
121
|
+
tester.pause
|
|
122
|
+
end
|
|
123
|
+
another_adapter.create_record(another_record) # expected no error raised
|
|
124
|
+
tester.finish
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it 'allows searching for the records on various criteria' do
|
|
128
|
+
lock = Coordinator::AutoExecuteLock.new(world)
|
|
129
|
+
adapter.create_record(lock)
|
|
130
|
+
found_records = adapter.find_records(owner_id: lock.owner_id)
|
|
131
|
+
found_records.size.must_equal 1
|
|
132
|
+
found_records.first.must_equal lock.data
|
|
133
|
+
|
|
134
|
+
found_records = adapter.find_records(class: lock.class.name, id: lock.id)
|
|
135
|
+
found_records.size.must_equal 1
|
|
136
|
+
found_records.first.must_equal lock.data
|
|
137
|
+
|
|
138
|
+
another_lock = Coordinator::AutoExecuteLock.new(another_world)
|
|
139
|
+
found_records = adapter.find_records(owner_id: another_lock.owner_id)
|
|
140
|
+
found_records.size.must_equal 0
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
describe CoordinatorAdapters::Sequel do
|
|
146
|
+
let(:adapter) { CoordinatorAdapters::Sequel.new(world) }
|
|
147
|
+
let(:another_adapter) { CoordinatorAdapters::Sequel.new(another_world) }
|
|
148
|
+
it_supports_global_records
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
require_relative 'test_helper'
|
|
2
|
+
|
|
3
|
+
module Dynflow
|
|
4
|
+
module DispatcherTest
|
|
5
|
+
describe "dispatcher" do
|
|
6
|
+
|
|
7
|
+
include TestHelpers
|
|
8
|
+
|
|
9
|
+
let(:persistence_adapter) { WorldFactory.persistence_adapter }
|
|
10
|
+
|
|
11
|
+
def self.dispatcher_works_with_this_connector
|
|
12
|
+
describe 'connector basics' do
|
|
13
|
+
before do
|
|
14
|
+
# just mention the executor to initialize it
|
|
15
|
+
executor_world
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe 'execution passing' do
|
|
19
|
+
it 'succeeds when expected' do
|
|
20
|
+
result = client_world.trigger(Support::DummyExample::Dummy)
|
|
21
|
+
assert_equal :success, result.finished.value.result
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
describe 'event passing' do
|
|
26
|
+
it 'succeeds when expected' do
|
|
27
|
+
result = client_world.trigger(Support::DummyExample::EventedAction, :timeout => 3)
|
|
28
|
+
step = wait_for do
|
|
29
|
+
client_world.persistence.load_execution_plan(result.id).
|
|
30
|
+
steps_in_state(:suspended).first
|
|
31
|
+
end
|
|
32
|
+
client_world.event(step.execution_plan_id, step.id, 'finish')
|
|
33
|
+
plan = result.finished.value
|
|
34
|
+
assert_equal('finish', plan.actions.first.output[:event])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'fails the future when the step is not accepting events' do
|
|
38
|
+
result = client_world.trigger(Support::CodeWorkflowExample::Dummy, { :text => "dummy" })
|
|
39
|
+
plan = result.finished.value!
|
|
40
|
+
step = plan.steps.values.first
|
|
41
|
+
future = client_world.event(plan.id, step.id, 'finish')
|
|
42
|
+
future.wait
|
|
43
|
+
assert future.failed?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'succeeds when executor acts as client' do
|
|
47
|
+
result = client_world.trigger(Support::DummyExample::ComposedAction, :timeout => 3)
|
|
48
|
+
plan = result.finished.value
|
|
49
|
+
assert_equal('finish', plan.actions.first.output[:event])
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.supports_dynamic_retry
|
|
56
|
+
before do
|
|
57
|
+
# mention the executors to make sure they are initialized
|
|
58
|
+
@executors = [executor_world, executor_world_2]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
describe 'when some executor is terminated and client is notified about the failure' do
|
|
62
|
+
specify 'client passes the work to another executor' do
|
|
63
|
+
triggered = while_executing_plan { |executor| executor.terminate.wait }
|
|
64
|
+
plan = finish_the_plan(triggered)
|
|
65
|
+
assert_plan_reexecuted(plan)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.supports_ping_pong
|
|
72
|
+
describe 'ping/pong' do
|
|
73
|
+
it 'succeeds when the world is available' do
|
|
74
|
+
ping_response = client_world.ping(executor_world.id, 0.5)
|
|
75
|
+
ping_response.wait
|
|
76
|
+
assert ping_response.success?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'time-outs when the world is not responding' do
|
|
80
|
+
executor_world.terminate.wait
|
|
81
|
+
ping_response = client_world.ping(executor_world.id, 0.5)
|
|
82
|
+
ping_response.wait
|
|
83
|
+
assert ping_response.failed?
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def self.handles_no_executor_available
|
|
89
|
+
it 'fails to finish the future when no executor available' do
|
|
90
|
+
client_world # just to initialize the client world before terminating the executors
|
|
91
|
+
executor_world.terminate.wait
|
|
92
|
+
executor_world_2.terminate.wait
|
|
93
|
+
result = client_world.trigger(Support::DummyExample::Dummy)
|
|
94
|
+
result.finished.wait
|
|
95
|
+
assert result.finished.failed?
|
|
96
|
+
assert_match(/No executor available/, result.finished.reason.message)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
describe 'direct connector - all in one' do
|
|
101
|
+
let(:connector) { Proc.new { |world| Connectors::Direct.new(world) } }
|
|
102
|
+
let(:executor_world) { create_world }
|
|
103
|
+
let(:client_world) { executor_world }
|
|
104
|
+
|
|
105
|
+
dispatcher_works_with_this_connector
|
|
106
|
+
supports_ping_pong
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
describe 'direct connector - multi executor multi client' do
|
|
110
|
+
let(:shared_connector) { Connectors::Direct.new() }
|
|
111
|
+
let(:connector) { Proc.new { |world| shared_connector.start_listening(world); shared_connector } }
|
|
112
|
+
let(:executor_world) { create_world(true) }
|
|
113
|
+
let(:executor_world_2) { create_world(true) }
|
|
114
|
+
let(:client_world) { create_world(false) }
|
|
115
|
+
let(:client_world_2) { create_world(false) }
|
|
116
|
+
|
|
117
|
+
dispatcher_works_with_this_connector
|
|
118
|
+
supports_dynamic_retry
|
|
119
|
+
supports_ping_pong
|
|
120
|
+
handles_no_executor_available
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
describe 'database connector - all in one' do
|
|
124
|
+
let(:connector) { Proc.new { |world| Connectors::Database.new(world, connector_polling_interval(world)) } }
|
|
125
|
+
let(:executor_world) { create_world }
|
|
126
|
+
let(:client_world) { executor_world }
|
|
127
|
+
|
|
128
|
+
dispatcher_works_with_this_connector
|
|
129
|
+
supports_ping_pong
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
describe 'database connector - multi executor multi client' do
|
|
133
|
+
let(:connector) { Proc.new { |world| Connectors::Database.new(world, connector_polling_interval(world)) } }
|
|
134
|
+
let(:executor_world) { create_world(true) }
|
|
135
|
+
let(:executor_world_2) { create_world(true) }
|
|
136
|
+
let(:client_world) { create_world(false) }
|
|
137
|
+
let(:client_world_2) { create_world(false) }
|
|
138
|
+
|
|
139
|
+
dispatcher_works_with_this_connector
|
|
140
|
+
supports_dynamic_retry
|
|
141
|
+
supports_ping_pong
|
|
142
|
+
handles_no_executor_available
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|