cap-ext-parallelize 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -0,0 +1,4 @@
1
+ ---
2
+ :minor: 1
3
+ :patch: 2
4
+ :major: 0
@@ -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
@@ -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
+