cap-ext-parallelize 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|