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