dynflow 0.7.9 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (118) hide show
  1. data/.gitignore +2 -0
  2. data/.travis.yml +16 -1
  3. data/Gemfile +13 -1
  4. data/doc/pages/source/_drafts/2015-03-01-new-documentation.markdown +10 -0
  5. data/doc/pages/source/_includes/menu.html +1 -0
  6. data/doc/pages/source/_includes/menu_right.html +1 -1
  7. data/doc/pages/source/_sass/_bootstrap-variables.sass +1 -0
  8. data/doc/pages/source/_sass/_style.scss +4 -0
  9. data/doc/pages/source/blog/index.html +12 -0
  10. data/doc/pages/source/documentation/index.md +330 -5
  11. data/dynflow.gemspec +3 -1
  12. data/examples/example_helper.rb +18 -11
  13. data/examples/orchestrate_evented.rb +2 -1
  14. data/examples/remote_executor.rb +53 -20
  15. data/lib/dynflow.rb +16 -6
  16. data/lib/dynflow/action/suspended.rb +1 -1
  17. data/lib/dynflow/action/with_sub_plans.rb +3 -6
  18. data/lib/dynflow/actor.rb +56 -0
  19. data/lib/dynflow/clock.rb +43 -38
  20. data/lib/dynflow/config.rb +107 -0
  21. data/lib/dynflow/connectors.rb +7 -0
  22. data/lib/dynflow/connectors/abstract.rb +41 -0
  23. data/lib/dynflow/connectors/database.rb +175 -0
  24. data/lib/dynflow/connectors/direct.rb +71 -0
  25. data/lib/dynflow/coordinator.rb +280 -0
  26. data/lib/dynflow/coordinator_adapters.rb +8 -0
  27. data/lib/dynflow/coordinator_adapters/abstract.rb +28 -0
  28. data/lib/dynflow/coordinator_adapters/sequel.rb +29 -0
  29. data/lib/dynflow/dispatcher.rb +58 -0
  30. data/lib/dynflow/dispatcher/abstract.rb +14 -0
  31. data/lib/dynflow/dispatcher/client_dispatcher.rb +139 -0
  32. data/lib/dynflow/dispatcher/executor_dispatcher.rb +86 -0
  33. data/lib/dynflow/errors.rb +7 -1
  34. data/lib/dynflow/execution_history.rb +46 -0
  35. data/lib/dynflow/execution_plan.rb +19 -15
  36. data/lib/dynflow/executors.rb +0 -1
  37. data/lib/dynflow/executors/abstract.rb +5 -10
  38. data/lib/dynflow/executors/parallel.rb +16 -13
  39. data/lib/dynflow/executors/parallel/core.rb +76 -78
  40. data/lib/dynflow/executors/parallel/execution_plan_manager.rb +4 -5
  41. data/lib/dynflow/executors/parallel/pool.rb +22 -52
  42. data/lib/dynflow/executors/parallel/running_steps_manager.rb +9 -2
  43. data/lib/dynflow/executors/parallel/worker.rb +5 -10
  44. data/lib/dynflow/persistence.rb +14 -0
  45. data/lib/dynflow/persistence_adapters/abstract.rb +14 -3
  46. data/lib/dynflow/persistence_adapters/sequel.rb +142 -38
  47. data/lib/dynflow/persistence_adapters/sequel_migrations/004_coordinator_records.rb +14 -0
  48. data/lib/dynflow/persistence_adapters/sequel_migrations/005_envelopes.rb +14 -0
  49. data/lib/dynflow/round_robin.rb +37 -0
  50. data/lib/dynflow/serializable.rb +1 -2
  51. data/lib/dynflow/serializer.rb +46 -0
  52. data/lib/dynflow/testing/dummy_executor.rb +2 -2
  53. data/lib/dynflow/testing/dummy_world.rb +1 -1
  54. data/lib/dynflow/transaction_adapters/abstract.rb +0 -5
  55. data/lib/dynflow/transaction_adapters/active_record.rb +0 -10
  56. data/lib/dynflow/version.rb +1 -1
  57. data/lib/dynflow/web.rb +26 -0
  58. data/lib/dynflow/web/console.rb +108 -0
  59. data/lib/dynflow/web/console_helpers.rb +158 -0
  60. data/lib/dynflow/web/filtering_helpers.rb +85 -0
  61. data/lib/dynflow/web/world_helpers.rb +9 -0
  62. data/lib/dynflow/web_console.rb +3 -310
  63. data/lib/dynflow/world.rb +188 -119
  64. data/test/abnormal_states_recovery_test.rb +152 -0
  65. data/test/action_test.rb +2 -3
  66. data/test/clock_test.rb +1 -5
  67. data/test/coordinator_test.rb +152 -0
  68. data/test/dispatcher_test.rb +146 -0
  69. data/test/execution_plan_test.rb +2 -1
  70. data/test/executor_test.rb +534 -612
  71. data/test/middleware_test.rb +4 -4
  72. data/test/persistence_test.rb +17 -0
  73. data/test/prepare_travis_env.sh +35 -0
  74. data/test/rescue_test.rb +5 -3
  75. data/test/round_robin_test.rb +28 -0
  76. data/test/support/code_workflow_example.rb +0 -73
  77. data/test/support/dummy_example.rb +130 -0
  78. data/test/support/test_execution_log.rb +41 -0
  79. data/test/test_helper.rb +222 -116
  80. data/test/testing_test.rb +10 -10
  81. data/test/web_console_test.rb +3 -3
  82. data/test/world_test.rb +23 -0
  83. data/web/assets/images/logo-square.png +0 -0
  84. data/web/assets/stylesheets/application.css +9 -0
  85. data/web/assets/vendor/bootstrap/config.json +429 -0
  86. data/web/assets/vendor/bootstrap/css/bootstrap-theme.css +479 -0
  87. data/web/assets/vendor/bootstrap/css/bootstrap-theme.min.css +10 -0
  88. data/web/assets/vendor/bootstrap/css/bootstrap.css +5377 -4980
  89. data/web/assets/vendor/bootstrap/css/bootstrap.min.css +9 -8
  90. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.eot +0 -0
  91. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.svg +288 -0
  92. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf +0 -0
  93. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff +0 -0
  94. data/web/assets/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff2 +0 -0
  95. data/web/assets/vendor/bootstrap/js/bootstrap.js +1674 -1645
  96. data/web/assets/vendor/bootstrap/js/bootstrap.min.js +11 -5
  97. data/web/views/execution_history.erb +17 -0
  98. data/web/views/index.erb +4 -6
  99. data/web/views/layout.erb +44 -8
  100. data/web/views/show.erb +4 -5
  101. data/web/views/worlds.erb +26 -0
  102. metadata +116 -23
  103. checksums.yaml +0 -15
  104. data/lib/dynflow/daemon.rb +0 -30
  105. data/lib/dynflow/executors/remote_via_socket.rb +0 -43
  106. data/lib/dynflow/executors/remote_via_socket/core.rb +0 -184
  107. data/lib/dynflow/future.rb +0 -173
  108. data/lib/dynflow/listeners.rb +0 -7
  109. data/lib/dynflow/listeners/abstract.rb +0 -17
  110. data/lib/dynflow/listeners/serialization.rb +0 -77
  111. data/lib/dynflow/listeners/socket.rb +0 -117
  112. data/lib/dynflow/micro_actor.rb +0 -102
  113. data/lib/dynflow/simple_world.rb +0 -19
  114. data/test/remote_via_socket_test.rb +0 -170
  115. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.css +0 -1109
  116. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.min.css +0 -9
  117. data/web/assets/vendor/bootstrap/img/glyphicons-halflings-white.png +0 -0
  118. data/web/assets/vendor/bootstrap/img/glyphicons-halflings.png +0 -0
@@ -4,7 +4,7 @@ module Dynflow
4
4
  module MiddlewareTest
5
5
 
6
6
  describe 'Middleware' do
7
- let(:world) { WorldInstance.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
- WorldInstance.create_world.tap do |world|
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
- WorldInstance.create_world.tap do |world|
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 = WorldInstance.create_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
@@ -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
- include WorldInstance
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
- def world
119
- @world ||= WorldInstance.create_world(auto_rescue: true)
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
- class TestExecutionLog
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 = Dynflow::Future.new
68
- @ready = Dynflow::Future.new
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.ready?
42
+ elsif @ready.completed?
81
43
  raise 'you can pause only once'
82
44
  else
83
- @ready.resolve(true)
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.resolve(true) # resume the run
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
- module WorldInstance
101
- def self.world
102
- @world ||= create_world
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 self.remote_world
106
- return @remote_world if @remote_world
107
- @listener, @remote_world = create_remote_world world
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.create_world(options = {})
116
- options = { pool_size: 5,
117
- persistence_adapter: Dynflow::PersistenceAdapters::Sequel.new('sqlite:/'),
118
- transaction_adapter: Dynflow::TransactionAdapters::None.new,
119
- exit_on_terminate: false,
120
- logger_adapter: logger_adapter,
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.create_remote_world(world)
126
- @counter ||= 0
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.terminate
142
- remote_world.terminate.wait if @remote_world
143
- world.terminate.wait if @world
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
- @remote_world = @world = nil
132
+ def self.terminate_worlds
133
+ created_worlds.map(&:terminate).map(&:wait)
134
+ created_worlds.clear
146
135
  end
136
+ end
147
137
 
148
- def world
149
- WorldInstance.world
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 remote_world
153
- WorldInstance.remote_world
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
- # ensure there are no unresolved Futures at the end or being GCed
158
- future_tests =-> do
159
- future_creations = {}
160
- non_ready_futures = {}
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
- MiniTest.after_run do
163
- WorldInstance.terminate
164
- futures = ObjectSpace.each_object(Dynflow::Future).select { |f| !f.ready? }
165
- unless futures.empty?
166
- raise "there are unready futures:\n" +
167
- futures.map { |f| "#{f}\n#{future_creations[f.object_id]}" }.join("\n")
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
- Dynflow::Future.singleton_class.send :define_method, :new do |*args, &block|
172
- super(*args, &block).tap do |f|
173
- future_creations[f.object_id] = caller(3)
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
- set_method = Dynflow::Future.instance_method :set
179
- Dynflow::Future.send :define_method, :set do |*args|
180
- begin
181
- set_method.bind(self).call *args
182
- ensure
183
- non_ready_futures.delete self.object_id
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
- unless non_ready_futures.empty?
189
- unified = non_ready_futures.each_with_object({}) do |(id, _), h|
190
- backtrace_first = future_creations[id][0]
191
- h[backtrace_first] ||= []
192
- h[backtrace_first] << id
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
- raise("there were #{non_ready_futures.size} non_ready_futures:\n" +
195
- unified.map do |backtrace, ids|
196
- "--- #{ids.size}: #{ids}\n#{future_creations[ids.first].join("\n")}"
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 = Dynflow::Future.instance_method(:wait)
284
+ wait_method = Concurrent::Edge::Event.instance_method(:wait)
204
285
 
205
- Dynflow::Future.class_eval do
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.call
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