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