dynflow 0.7.9 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. data/.gitignore +2 -0
  2. data/.travis.yml +16 -1
  3. data/Gemfile +13 -1
  4. data/doc/pages/source/_drafts/2015-03-01-new-documentation.markdown +10 -0
  5. data/doc/pages/source/_includes/menu.html +1 -0
  6. data/doc/pages/source/_includes/menu_right.html +1 -1
  7. data/doc/pages/source/_sass/_bootstrap-variables.sass +1 -0
  8. data/doc/pages/source/_sass/_style.scss +4 -0
  9. data/doc/pages/source/blog/index.html +12 -0
  10. data/doc/pages/source/documentation/index.md +330 -5
  11. data/dynflow.gemspec +3 -1
  12. data/examples/example_helper.rb +18 -11
  13. data/examples/orchestrate_evented.rb +2 -1
  14. data/examples/remote_executor.rb +53 -20
  15. data/lib/dynflow.rb +16 -6
  16. data/lib/dynflow/action/suspended.rb +1 -1
  17. data/lib/dynflow/action/with_sub_plans.rb +3 -6
  18. data/lib/dynflow/actor.rb +56 -0
  19. data/lib/dynflow/clock.rb +43 -38
  20. data/lib/dynflow/config.rb +107 -0
  21. data/lib/dynflow/connectors.rb +7 -0
  22. data/lib/dynflow/connectors/abstract.rb +41 -0
  23. data/lib/dynflow/connectors/database.rb +175 -0
  24. data/lib/dynflow/connectors/direct.rb +71 -0
  25. data/lib/dynflow/coordinator.rb +280 -0
  26. data/lib/dynflow/coordinator_adapters.rb +8 -0
  27. data/lib/dynflow/coordinator_adapters/abstract.rb +28 -0
  28. data/lib/dynflow/coordinator_adapters/sequel.rb +29 -0
  29. data/lib/dynflow/dispatcher.rb +58 -0
  30. data/lib/dynflow/dispatcher/abstract.rb +14 -0
  31. data/lib/dynflow/dispatcher/client_dispatcher.rb +139 -0
  32. data/lib/dynflow/dispatcher/executor_dispatcher.rb +86 -0
  33. data/lib/dynflow/errors.rb +7 -1
  34. data/lib/dynflow/execution_history.rb +46 -0
  35. data/lib/dynflow/execution_plan.rb +19 -15
  36. data/lib/dynflow/executors.rb +0 -1
  37. data/lib/dynflow/executors/abstract.rb +5 -10
  38. data/lib/dynflow/executors/parallel.rb +16 -13
  39. data/lib/dynflow/executors/parallel/core.rb +76 -78
  40. data/lib/dynflow/executors/parallel/execution_plan_manager.rb +4 -5
  41. data/lib/dynflow/executors/parallel/pool.rb +22 -52
  42. data/lib/dynflow/executors/parallel/running_steps_manager.rb +9 -2
  43. data/lib/dynflow/executors/parallel/worker.rb +5 -10
  44. data/lib/dynflow/persistence.rb +14 -0
  45. data/lib/dynflow/persistence_adapters/abstract.rb +14 -3
  46. data/lib/dynflow/persistence_adapters/sequel.rb +142 -38
  47. data/lib/dynflow/persistence_adapters/sequel_migrations/004_coordinator_records.rb +14 -0
  48. data/lib/dynflow/persistence_adapters/sequel_migrations/005_envelopes.rb +14 -0
  49. data/lib/dynflow/round_robin.rb +37 -0
  50. data/lib/dynflow/serializable.rb +1 -2
  51. data/lib/dynflow/serializer.rb +46 -0
  52. data/lib/dynflow/testing/dummy_executor.rb +2 -2
  53. data/lib/dynflow/testing/dummy_world.rb +1 -1
  54. data/lib/dynflow/transaction_adapters/abstract.rb +0 -5
  55. data/lib/dynflow/transaction_adapters/active_record.rb +0 -10
  56. data/lib/dynflow/version.rb +1 -1
  57. data/lib/dynflow/web.rb +26 -0
  58. data/lib/dynflow/web/console.rb +108 -0
  59. data/lib/dynflow/web/console_helpers.rb +158 -0
  60. data/lib/dynflow/web/filtering_helpers.rb +85 -0
  61. data/lib/dynflow/web/world_helpers.rb +9 -0
  62. data/lib/dynflow/web_console.rb +3 -310
  63. data/lib/dynflow/world.rb +188 -119
  64. data/test/abnormal_states_recovery_test.rb +152 -0
  65. data/test/action_test.rb +2 -3
  66. data/test/clock_test.rb +1 -5
  67. data/test/coordinator_test.rb +152 -0
  68. data/test/dispatcher_test.rb +146 -0
  69. data/test/execution_plan_test.rb +2 -1
  70. data/test/executor_test.rb +534 -612
  71. data/test/middleware_test.rb +4 -4
  72. data/test/persistence_test.rb +17 -0
  73. data/test/prepare_travis_env.sh +35 -0
  74. data/test/rescue_test.rb +5 -3
  75. data/test/round_robin_test.rb +28 -0
  76. data/test/support/code_workflow_example.rb +0 -73
  77. data/test/support/dummy_example.rb +130 -0
  78. data/test/support/test_execution_log.rb +41 -0
  79. data/test/test_helper.rb +222 -116
  80. data/test/testing_test.rb +10 -10
  81. data/test/web_console_test.rb +3 -3
  82. data/test/world_test.rb +23 -0
  83. data/web/assets/images/logo-square.png +0 -0
  84. data/web/assets/stylesheets/application.css +9 -0
  85. data/web/assets/vendor/bootstrap/config.json +429 -0
  86. data/web/assets/vendor/bootstrap/css/bootstrap-theme.css +479 -0
  87. data/web/assets/vendor/bootstrap/css/bootstrap-theme.min.css +10 -0
  88. data/web/assets/vendor/bootstrap/css/bootstrap.css +5377 -4980
  89. data/web/assets/vendor/bootstrap/css/bootstrap.min.css +9 -8
  90. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.eot +0 -0
  91. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.svg +288 -0
  92. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf +0 -0
  93. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff +0 -0
  94. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff2 +0 -0
  95. data/web/assets/vendor/bootstrap/js/bootstrap.js +1674 -1645
  96. data/web/assets/vendor/bootstrap/js/bootstrap.min.js +11 -5
  97. data/web/views/execution_history.erb +17 -0
  98. data/web/views/index.erb +4 -6
  99. data/web/views/layout.erb +44 -8
  100. data/web/views/show.erb +4 -5
  101. data/web/views/worlds.erb +26 -0
  102. metadata +116 -23
  103. checksums.yaml +0 -15
  104. data/lib/dynflow/daemon.rb +0 -30
  105. data/lib/dynflow/executors/remote_via_socket.rb +0 -43
  106. data/lib/dynflow/executors/remote_via_socket/core.rb +0 -184
  107. data/lib/dynflow/future.rb +0 -173
  108. data/lib/dynflow/listeners.rb +0 -7
  109. data/lib/dynflow/listeners/abstract.rb +0 -17
  110. data/lib/dynflow/listeners/serialization.rb +0 -77
  111. data/lib/dynflow/listeners/socket.rb +0 -117
  112. data/lib/dynflow/micro_actor.rb +0 -102
  113. data/lib/dynflow/simple_world.rb +0 -19
  114. data/test/remote_via_socket_test.rb +0 -170
  115. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.css +0 -1109
  116. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.min.css +0 -9
  117. data/web/assets/vendor/bootstrap/img/glyphicons-halflings-white.png +0 -0
  118. data/web/assets/vendor/bootstrap/img/glyphicons-halflings.png +0 -0
@@ -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
- include WorldInstance
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.new Logger.new($stderr) }
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