cap-ext-parallelize 0.1.2
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/README.md +66 -0
- data/Rakefile +33 -0
- data/VERSION.yml +4 -0
- data/lib/cap_ext_parallelize.rb +9 -0
- data/lib/capistrano/configuration/extensions/actions/invocation.rb +84 -0
- data/lib/capistrano/configuration/extensions/connections.rb +57 -0
- data/lib/capistrano/configuration/extensions/execution.rb +49 -0
- data/test/parallel_invocation_test.rb +277 -0
- data/test/utils.rb +38 -0
- metadata +85 -0
data/README.md
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
cap-ext-parallelize - A Capistrano extension for parallel task execution
|
2
|
+
=============
|
3
|
+
|
4
|
+
Imagine you want to restart several processes, either on one or on different
|
5
|
+
servers, but that flingin flangin Capistrano just doesn't want to run all the
|
6
|
+
restart tasks in parallel.
|
7
|
+
|
8
|
+
I know what you're saying, Capistrano already has a command called `parallel`.
|
9
|
+
That should do right? Not exactly, we wanted to be able to run complete tasks,
|
10
|
+
no arbitrary blocks in parallel. The command `parallel` is only able to run
|
11
|
+
specific shell commands, and it looks weird when you want to run several of
|
12
|
+
them on only one host.
|
13
|
+
|
14
|
+
We were inspired by the syntax though, so when you want to run arbitrary blocks
|
15
|
+
in your Capistrano tasks, you can do it like this:
|
16
|
+
|
17
|
+
parallelize do |session|
|
18
|
+
session.run {deploy.restart}
|
19
|
+
session.run {queue.restart}
|
20
|
+
session.run {daemon.restart}
|
21
|
+
end
|
22
|
+
|
23
|
+
Every task will be run in its own thread, opening a new connection to the server.
|
24
|
+
Because of this you should be aware of potential resource exhaustion. You can
|
25
|
+
limit the number of threads in two ways, either set a variable (it defaults
|
26
|
+
to 10):
|
27
|
+
|
28
|
+
set :parallelize_thread_count, 10
|
29
|
+
|
30
|
+
Or specify it with a parameter:
|
31
|
+
|
32
|
+
parallelize(5) do
|
33
|
+
...
|
34
|
+
end
|
35
|
+
|
36
|
+
If one of your tasks ran in a transaction block and issued a rollback,
|
37
|
+
parallelize will rollback all other threads, if they have rollback statements
|
38
|
+
defined.
|
39
|
+
|
40
|
+
Known Issues
|
41
|
+
============
|
42
|
+
|
43
|
+
Due to the threading you have to be sure to already have authenticated your SSH
|
44
|
+
key (you're using SSH keys, right?) using an SSH agent before you run the
|
45
|
+
parallelized code. Otherwise it'll blow up, let's just leave it at that. That'll
|
46
|
+
change in the future.
|
47
|
+
|
48
|
+
Installation
|
49
|
+
============
|
50
|
+
|
51
|
+
1. Install the gem
|
52
|
+
|
53
|
+
gem install -s http://gems.github.com mattmatt-cap-ext-parallelize
|
54
|
+
|
55
|
+
2. Add the following line to your Capfile
|
56
|
+
|
57
|
+
require 'cap\_ext\_parallelize'
|
58
|
+
|
59
|
+
3. There is no step 3
|
60
|
+
|
61
|
+
License
|
62
|
+
=======
|
63
|
+
|
64
|
+
(c) 2009 Mathias Meyer, Jonathan Weiss
|
65
|
+
|
66
|
+
MIT-License
|
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/testtask'
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'jeweler'
|
7
|
+
Jeweler::Tasks.new do |s|
|
8
|
+
s.name = 'cap-ext-parallelize'
|
9
|
+
s.summary = 'A drop-in replacement for Capistrano to fire off Webistrano deployments transparently without losing the joy of using the cap command.'
|
10
|
+
s.email = 'meyer@paperplanes.de'
|
11
|
+
s.homepage = 'http://github.com/mattmatt/cap-ext-parallelize'
|
12
|
+
s.authors = ["Mathias Meyer"]
|
13
|
+
s.files = FileList["[A-Z]*", "{lib,test}/**/*"]
|
14
|
+
s.add_dependency 'capistrano'
|
15
|
+
end
|
16
|
+
rescue LoadError
|
17
|
+
puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
18
|
+
end
|
19
|
+
|
20
|
+
desc "Default Task"
|
21
|
+
task :default => ["test"]
|
22
|
+
|
23
|
+
desc "Runs the unit tests"
|
24
|
+
task :test => "test:unit"
|
25
|
+
|
26
|
+
namespace :test do
|
27
|
+
desc "Unit tests"
|
28
|
+
Rake::TestTask.new(:unit) do |t|
|
29
|
+
t.libs << 'test/unit'
|
30
|
+
t.pattern = "test/*_shoulda.rb"
|
31
|
+
t.verbose = true
|
32
|
+
end
|
33
|
+
end
|
data/VERSION.yml
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/capistrano/configuration/extensions/actions/invocation"
|
2
|
+
require "#{File.dirname(__FILE__)}/capistrano/configuration/extensions/connections"
|
3
|
+
require "#{File.dirname(__FILE__)}/capistrano/configuration/extensions/execution"
|
4
|
+
|
5
|
+
class Capistrano::Configuration
|
6
|
+
include Capistrano::Configuration::Extensions::Actions::Invocation
|
7
|
+
include Capistrano::Configuration::Extensions::Connections
|
8
|
+
include Capistrano::Configuration::Extensions::Execution
|
9
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Capistrano
|
2
|
+
class Configuration
|
3
|
+
module Extensions
|
4
|
+
module Actions
|
5
|
+
module Invocation
|
6
|
+
|
7
|
+
class BlockProxy
|
8
|
+
attr_accessor :blocks
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@blocks = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def run(&block)
|
15
|
+
blocks << block
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def parallelize(thread_count = nil)
|
20
|
+
set :parallelize_thread_count, 10 unless respond_to?(:parallelize_thread_count)
|
21
|
+
|
22
|
+
proxy = BlockProxy.new
|
23
|
+
yield proxy
|
24
|
+
|
25
|
+
logger.info "Running #{proxy.blocks.size} threads in chunks of #{thread_count || parallelize_thread_count}"
|
26
|
+
run_parallelize_loop(proxy, thread_count || parallelize_thread_count)
|
27
|
+
end
|
28
|
+
|
29
|
+
def run_parallelize_loop(proxy, thread_count)
|
30
|
+
batch = 1
|
31
|
+
all_threads = []
|
32
|
+
proxy.blocks.each_slice(thread_count) do |chunk|
|
33
|
+
logger.info "Running batch number #{batch}"
|
34
|
+
threads = run_in_threads(chunk)
|
35
|
+
all_threads << threads
|
36
|
+
wait_for(threads)
|
37
|
+
rollback_all_threads(all_threads.flatten) and return if threads.any? {|t| t[:rolled_back] || t[:exception_raised]}
|
38
|
+
batch += 1
|
39
|
+
end
|
40
|
+
all_threads
|
41
|
+
end
|
42
|
+
|
43
|
+
def run_in_threads(blocks)
|
44
|
+
blocks.collect do |blk|
|
45
|
+
thread = Thread.new do
|
46
|
+
logger.info "Running block in background thread"
|
47
|
+
blk.call
|
48
|
+
end
|
49
|
+
begin
|
50
|
+
thread.run
|
51
|
+
rescue ThreadError
|
52
|
+
thread[:exception_raised] = $!
|
53
|
+
end
|
54
|
+
thread
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def wait_for(threads)
|
59
|
+
threads.each do |thread|
|
60
|
+
begin
|
61
|
+
thread.join
|
62
|
+
rescue
|
63
|
+
logger.important "Subthread failed: #{$!.message}"
|
64
|
+
thread[:exception_raised] = $!
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def rollback_all_threads(threads)
|
70
|
+
Thread.new do
|
71
|
+
threads.select {|t| !t[:rolled_back]}.each do |thread|
|
72
|
+
Thread.current[:rollback_requests] = thread[:rollback_requests]
|
73
|
+
rollback!
|
74
|
+
end
|
75
|
+
end.join
|
76
|
+
rollback! # Rolling back main thread too
|
77
|
+
true
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Capistrano
|
2
|
+
class Configuration
|
3
|
+
module Extensions
|
4
|
+
|
5
|
+
# Thread-safe(r) version of the Capistrano default
|
6
|
+
# connection handling.
|
7
|
+
module Connections
|
8
|
+
def initialize_with_connections(*args) #:nodoc:
|
9
|
+
initialize_without_connections(*args)
|
10
|
+
Thread.current[:sessions] = {}
|
11
|
+
Thread.current[:failed_sessions] = []
|
12
|
+
end
|
13
|
+
|
14
|
+
# Indicate that the given server could not be connected to.
|
15
|
+
def failed!(server)
|
16
|
+
Thread.current[:failed_sessions] << server
|
17
|
+
end
|
18
|
+
|
19
|
+
# A hash of the SSH sessions that are currently open and available.
|
20
|
+
# Because sessions are constructed lazily, this will only contain
|
21
|
+
# connections to those servers that have been the targets of one or more
|
22
|
+
# executed tasks.
|
23
|
+
def sessions
|
24
|
+
Thread.current[:sessions] ||= {}
|
25
|
+
end
|
26
|
+
|
27
|
+
# Query whether previous connection attempts to the given server have
|
28
|
+
# failed.
|
29
|
+
def has_failed?(server)
|
30
|
+
Thread.current[:failed_sessions].include?(server)
|
31
|
+
end
|
32
|
+
|
33
|
+
def teardown_connections_to(servers)
|
34
|
+
servers.each do |server|
|
35
|
+
sessions[server].close
|
36
|
+
sessions.delete(server)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
def establish_connection_to(server, failures=nil)
|
42
|
+
current_thread = Thread.current
|
43
|
+
Thread.new { safely_establish_connection_to(server, current_thread, failures) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def safely_establish_connection_to(server, thread, failures=nil)
|
47
|
+
thread[:sessions] ||= {}
|
48
|
+
thread[:sessions][server] ||= connection_factory.connect_to(server)
|
49
|
+
rescue Exception => err
|
50
|
+
raise unless failures
|
51
|
+
failures << { :server => server, :error => err }
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Capistrano
|
2
|
+
class Configuration
|
3
|
+
module Extensions
|
4
|
+
module Execution
|
5
|
+
|
6
|
+
def task_call_frames
|
7
|
+
Thread.current[:task_call_frames] ||= []
|
8
|
+
end
|
9
|
+
|
10
|
+
def rollback_requests=(rollback_requests)
|
11
|
+
Thread.current[:rollback_requests] = rollback_requests
|
12
|
+
end
|
13
|
+
|
14
|
+
def rollback_requests
|
15
|
+
Thread.current[:rollback_requests]
|
16
|
+
end
|
17
|
+
|
18
|
+
def current_task
|
19
|
+
all_task_call_frames = Thread.main[:task_call_frames] + task_call_frames
|
20
|
+
return nil if all_task_call_frames.empty?
|
21
|
+
all_task_call_frames.last.task
|
22
|
+
end
|
23
|
+
|
24
|
+
def transaction?
|
25
|
+
!(rollback_requests.nil? && Thread.main[:rollback_requests].nil?)
|
26
|
+
end
|
27
|
+
|
28
|
+
def transaction(&blk)
|
29
|
+
super do
|
30
|
+
self.rollback_requests = [] unless transaction?
|
31
|
+
blk.call
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def on_rollback(&block)
|
36
|
+
self.rollback_requests ||= [] if transaction?
|
37
|
+
super
|
38
|
+
end
|
39
|
+
|
40
|
+
def rollback!
|
41
|
+
return if rollback_requests.nil?
|
42
|
+
super
|
43
|
+
Thread.current[:rolled_back] = true
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,277 @@
|
|
1
|
+
require "utils"
|
2
|
+
require "capistrano/task_definition"
|
3
|
+
require "capistrano/configuration"
|
4
|
+
require "#{File.join(File.dirname(__FILE__), '..', 'lib')}/cap_ext_parallelize"
|
5
|
+
|
6
|
+
class ConfigurationActionsParallelInvocationTest < Test::Unit::TestCase
|
7
|
+
class MockConfig
|
8
|
+
attr_reader :roles
|
9
|
+
attr_reader :options
|
10
|
+
attr_accessor :debug
|
11
|
+
attr_accessor :dry_run
|
12
|
+
attr_reader :tasks, :namespaces, :fully_qualified_name, :parent
|
13
|
+
attr_reader :state, :original_initialize_called
|
14
|
+
attr_accessor :logger, :default_task
|
15
|
+
attr_accessor :parallelize_thread_count
|
16
|
+
|
17
|
+
def initialize(options)
|
18
|
+
@original_initialize_called = true
|
19
|
+
@tasks = {}
|
20
|
+
@namespaces = {}
|
21
|
+
@state = {}
|
22
|
+
@fully_qualified_name = options[:fqn]
|
23
|
+
@parent = options[:parent]
|
24
|
+
@logger = options.delete(:logger)
|
25
|
+
@options = {}
|
26
|
+
@parallelize_thread_count = 10
|
27
|
+
@roles = {}
|
28
|
+
end
|
29
|
+
|
30
|
+
def [](*args)
|
31
|
+
@options[*args]
|
32
|
+
end
|
33
|
+
|
34
|
+
def set(name, value)
|
35
|
+
@options[name] = value
|
36
|
+
end
|
37
|
+
|
38
|
+
def fetch(*args)
|
39
|
+
@options.fetch(*args)
|
40
|
+
end
|
41
|
+
|
42
|
+
include Capistrano::Configuration::Execution
|
43
|
+
include Capistrano::Configuration::Actions::Invocation
|
44
|
+
include Capistrano::Configuration::Extensions::Execution
|
45
|
+
include Capistrano::Configuration::Extensions::Actions::Invocation
|
46
|
+
include Capistrano::Configuration::Servers
|
47
|
+
include Capistrano::Configuration::Connections
|
48
|
+
end
|
49
|
+
|
50
|
+
def setup
|
51
|
+
@config = MockConfig.new(:logger => stub(:debug => nil, :info => nil, :important => nil, :trace => nil))
|
52
|
+
@original_io_proc = MockConfig.default_io_proc
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_parallelize_should_run_all_collected_tasks
|
56
|
+
aaa = new_task(@config, :aaa) do
|
57
|
+
parallelize do |session|
|
58
|
+
session.run {(state[:has_been_run] ||= []) << :first}
|
59
|
+
session.run {(state[:has_been_run] ||= []) << :second}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
@config.execute_task(aaa)
|
63
|
+
assert @config.state[:has_been_run].include?(:first)
|
64
|
+
assert @config.state[:has_been_run].include?(:second)
|
65
|
+
end
|
66
|
+
|
67
|
+
def test_parallelize_should_rollback_all_threads_when_one_thread_raises_error
|
68
|
+
ccc = new_task(@config, :ccc) do
|
69
|
+
on_rollback {(state[:rollback] ||= []) << :first}
|
70
|
+
raise "boom"
|
71
|
+
end
|
72
|
+
|
73
|
+
eee = new_task(@config, :eee) do
|
74
|
+
on_rollback {(state[:rollback] ||= []) << :second}
|
75
|
+
end
|
76
|
+
|
77
|
+
ddd = new_task(@config, :ddd) do
|
78
|
+
transaction {execute_task(eee)}
|
79
|
+
end
|
80
|
+
|
81
|
+
bbb = new_task(@config, :bbb) {transaction {execute_task(ccc)}}
|
82
|
+
|
83
|
+
aaa = new_task(@config, :aaa) do
|
84
|
+
on_rollback {puts 'rolled back'}
|
85
|
+
parallelize do |session|
|
86
|
+
session.run {execute_task(bbb)}
|
87
|
+
session.run {execute_task(ddd)}
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
@config.execute_task(aaa)
|
92
|
+
assert @config.state[:rollback].include?(:first)
|
93
|
+
assert @config.state[:rollback].include?(:second)
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_parallelize_should_rollback_only_run_threads_when_one_thread_raises_error
|
97
|
+
ccc = new_task(@config, :ccc) do
|
98
|
+
on_rollback {(state[:rollback] ||= []) << :first}
|
99
|
+
raise "boom"
|
100
|
+
end
|
101
|
+
|
102
|
+
eee = new_task(@config, :eee) do
|
103
|
+
on_rollback {(state[:rollback] ||= []) << :second}
|
104
|
+
end
|
105
|
+
|
106
|
+
ddd = new_task(@config, :ddd) do
|
107
|
+
transaction {execute_task(eee)}
|
108
|
+
end
|
109
|
+
|
110
|
+
bbb = new_task(@config, :bbb) {transaction {execute_task(ccc)}}
|
111
|
+
|
112
|
+
aaa = new_task(@config, :aaa) do
|
113
|
+
on_rollback {puts 'rolled back'}
|
114
|
+
parallelize do |session|
|
115
|
+
session.run {execute_task(bbb)}
|
116
|
+
session.run {execute_task(ddd)}
|
117
|
+
end
|
118
|
+
end
|
119
|
+
@config.parallelize_thread_count = 1
|
120
|
+
@config.execute_task(aaa)
|
121
|
+
assert @config.state[:rollback].include?(:first)
|
122
|
+
assert !@config.state[:rollback].include?(:second)
|
123
|
+
end
|
124
|
+
|
125
|
+
def test_parallelize_should_rollback_all_threads_when_one_thread_raises_error
|
126
|
+
ccc = new_task(@config, :ccc) do
|
127
|
+
on_rollback {(state[:rollback] ||= []) << :first}
|
128
|
+
sleep 0.1
|
129
|
+
raise "boom"
|
130
|
+
end
|
131
|
+
|
132
|
+
eee = new_task(@config, :eee) do
|
133
|
+
on_rollback {(state[:rollback] ||= []) << :second}
|
134
|
+
end
|
135
|
+
|
136
|
+
ddd = new_task(@config, :ddd) do
|
137
|
+
transaction {execute_task(eee)}
|
138
|
+
end
|
139
|
+
|
140
|
+
bbb = new_task(@config, :bbb) {transaction {execute_task(ccc)}}
|
141
|
+
|
142
|
+
aaa = new_task(@config, :aaa) do
|
143
|
+
on_rollback {puts 'rolled back'}
|
144
|
+
parallelize do |session|
|
145
|
+
session.run {execute_task(bbb)}
|
146
|
+
session.run {execute_task(ddd)}
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
@config.execute_task(aaa)
|
151
|
+
assert @config.state[:rollback].include?(:first)
|
152
|
+
assert @config.state[:rollback].include?(:second)
|
153
|
+
end
|
154
|
+
|
155
|
+
def test_should_not_rollback_threads_twice
|
156
|
+
ccc = new_task(@config, :ccc) do
|
157
|
+
on_rollback {(state[:rollback] ||= []) << :first}
|
158
|
+
raise "boom"
|
159
|
+
end
|
160
|
+
|
161
|
+
eee = new_task(@config, :eee) do
|
162
|
+
on_rollback {(state[:rollback] ||= []) << :second}
|
163
|
+
end
|
164
|
+
|
165
|
+
ddd = new_task(@config, :ddd) do
|
166
|
+
transaction {execute_task(eee)}
|
167
|
+
end
|
168
|
+
|
169
|
+
bbb = new_task(@config, :bbb) {transaction {execute_task(ccc)}}
|
170
|
+
|
171
|
+
aaa = new_task(@config, :aaa) do
|
172
|
+
on_rollback {puts 'rolled back'}
|
173
|
+
parallelize do |session|
|
174
|
+
session.run {execute_task(bbb)}
|
175
|
+
session.run {execute_task(ddd)}
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
@config.execute_task(aaa)
|
180
|
+
assert_equal 2, @config.state[:rollback].size
|
181
|
+
assert @config.state[:rollback].include?(:first)
|
182
|
+
assert @config.state[:rollback].include?(:second)
|
183
|
+
end
|
184
|
+
|
185
|
+
def test_should_rollback_main_thread_too
|
186
|
+
|
187
|
+
eee = new_task(@config, :eee) do
|
188
|
+
on_rollback {(state[:rollback] ||= []) << :second}
|
189
|
+
end
|
190
|
+
|
191
|
+
ddd = new_task(@config, :ddd) do
|
192
|
+
transaction {execute_task(eee)}
|
193
|
+
end
|
194
|
+
|
195
|
+
aaa = new_task(@config, :aaa) do
|
196
|
+
on_rollback {(state[:rollback] ||= []) << :main}
|
197
|
+
parallelize do |session|
|
198
|
+
session.run {execute_task(bbb)}
|
199
|
+
session.run {execute_task(ddd)}
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
bbb = new_task(@config, :bbb) do
|
204
|
+
transaction do
|
205
|
+
execute_task(aaa)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
@config.execute_task(bbb)
|
210
|
+
assert_equal 2, @config.state[:rollback].size
|
211
|
+
assert @config.state[:rollback].include?(:main)
|
212
|
+
assert @config.state[:rollback].include?(:second)
|
213
|
+
end
|
214
|
+
|
215
|
+
def test_should_run_each_run_block_in_separate_thread
|
216
|
+
bbb = new_task(@config, :bbb) do
|
217
|
+
# noop
|
218
|
+
end
|
219
|
+
|
220
|
+
ccc = new_task(@config, :ccc) do
|
221
|
+
# noop
|
222
|
+
end
|
223
|
+
|
224
|
+
@threads = []
|
225
|
+
aaa = new_task(@config, :aaa) do
|
226
|
+
return parallelize do |session|
|
227
|
+
session.run {execute_task(bbb)}
|
228
|
+
session.run {execute_task(ccc)}
|
229
|
+
end
|
230
|
+
end
|
231
|
+
@config.execute_task(aaa)
|
232
|
+
assert_equal 2, @threads.size
|
233
|
+
assert @threads.first.is_a?(Thread)
|
234
|
+
assert @threads.second.is_a?(Thread)
|
235
|
+
end
|
236
|
+
|
237
|
+
def test_should_respect_roles_configured_in_the_calling_task
|
238
|
+
web_server = role(@config, :web, "my.host")
|
239
|
+
bgrnd_server = role(@config, :daemons, "my.other.host")
|
240
|
+
|
241
|
+
main = new_task(@config, :aaa, :roles => :web) do
|
242
|
+
parallelize do |session|
|
243
|
+
session.run {run 'echo hello'}
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
@config.stubs(:connection_factory)
|
248
|
+
@config.expects(:establish_connection_to).with(web_server.first, []).returns(Thread.new {})
|
249
|
+
@config.execute_task(main)
|
250
|
+
end
|
251
|
+
|
252
|
+
def test_should_rollback_when_main_thread_has_transaction_and_subthread_has_error
|
253
|
+
bbb = new_task(@config, :bbb) do
|
254
|
+
on_rollback {(state[:rollback] ||= []) << :second}
|
255
|
+
raise
|
256
|
+
end
|
257
|
+
|
258
|
+
aaa = new_task(@config, :aaa) do
|
259
|
+
transaction do
|
260
|
+
parallelize do |session|
|
261
|
+
session.run {execute_task(bbb)}
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
@config.execute_task(aaa)
|
267
|
+
assert_equal 1, @config.state[:rollback].size
|
268
|
+
assert @config.state[:rollback].include?(:second)
|
269
|
+
end
|
270
|
+
|
271
|
+
private
|
272
|
+
def new_task(namespace, name, options={}, &block)
|
273
|
+
block ||= stack_inspector
|
274
|
+
namespace.tasks[name] = Capistrano::TaskDefinition.new(name, namespace, options, &block)
|
275
|
+
end
|
276
|
+
|
277
|
+
end
|
data/test/utils.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
begin
|
2
|
+
require 'rubygems'
|
3
|
+
gem 'mocha'
|
4
|
+
rescue LoadError
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'test/unit'
|
8
|
+
require 'mocha'
|
9
|
+
require 'capistrano/server_definition'
|
10
|
+
|
11
|
+
module TestExtensions
|
12
|
+
def server(host, options={})
|
13
|
+
Capistrano::ServerDefinition.new(host, options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def namespace(fqn=nil)
|
17
|
+
space = stub(:roles => {}, :fully_qualified_name => fqn, :default_task => nil)
|
18
|
+
yield(space) if block_given?
|
19
|
+
space
|
20
|
+
end
|
21
|
+
|
22
|
+
def role(space, name, *args)
|
23
|
+
opts = args.last.is_a?(Hash) ? args.pop : {}
|
24
|
+
space.roles[name] ||= []
|
25
|
+
space.roles[name].concat(args.map { |h| Capistrano::ServerDefinition.new(h, opts) })
|
26
|
+
end
|
27
|
+
|
28
|
+
def new_task(name, namespace=@namespace, options={}, &block)
|
29
|
+
block ||= Proc.new {}
|
30
|
+
task = Capistrano::TaskDefinition.new(name, namespace, options, &block)
|
31
|
+
assert_equal block, task.body
|
32
|
+
return task
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class Test::Unit::TestCase
|
37
|
+
include TestExtensions
|
38
|
+
end
|
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cap-ext-parallelize
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 2
|
9
|
+
version: 0.1.2
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Mathias Meyer
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2009-03-19 00:00:00 +00:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: capistrano
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
version: "0"
|
31
|
+
type: :runtime
|
32
|
+
version_requirements: *id001
|
33
|
+
description:
|
34
|
+
email: meyer@paperplanes.de
|
35
|
+
executables: []
|
36
|
+
|
37
|
+
extensions: []
|
38
|
+
|
39
|
+
extra_rdoc_files: []
|
40
|
+
|
41
|
+
files:
|
42
|
+
- Rakefile
|
43
|
+
- README.md
|
44
|
+
- VERSION.yml
|
45
|
+
- lib/cap_ext_parallelize.rb
|
46
|
+
- lib/capistrano/configuration/extensions/actions/invocation.rb
|
47
|
+
- lib/capistrano/configuration/extensions/connections.rb
|
48
|
+
- lib/capistrano/configuration/extensions/execution.rb
|
49
|
+
- test/parallel_invocation_test.rb
|
50
|
+
- test/utils.rb
|
51
|
+
has_rdoc: true
|
52
|
+
homepage: http://github.com/mattmatt/cap-ext-parallelize
|
53
|
+
licenses: []
|
54
|
+
|
55
|
+
post_install_message:
|
56
|
+
rdoc_options:
|
57
|
+
- --inline-source
|
58
|
+
- --charset=UTF-8
|
59
|
+
require_paths:
|
60
|
+
- lib
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
segments:
|
67
|
+
- 0
|
68
|
+
version: "0"
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
segments:
|
75
|
+
- 0
|
76
|
+
version: "0"
|
77
|
+
requirements: []
|
78
|
+
|
79
|
+
rubyforge_project:
|
80
|
+
rubygems_version: 1.3.7
|
81
|
+
signing_key:
|
82
|
+
specification_version: 2
|
83
|
+
summary: A drop-in replacement for Capistrano to fire off Webistrano deployments transparently without losing the joy of using the cap command.
|
84
|
+
test_files: []
|
85
|
+
|